当我将“ react.ement”作为道具时,如何避免运行多次使用效应?

发布于 2025-02-10 00:47:55 字数 2159 浏览 1 评论 0原文

我有一个list组件可以控制列表的同一逻辑。 我将渲染的React元素作为道具传递给list组件。

但是,在状态更改时,每次都会运行子女组件的使用效应。

简化的代码如下所示:

saga

export function* fetchPosts(action) {
  try {
    const res = yield call_api(action.url);
    yield put(actions.appendPosts(res.data.posts);  // state.posts = state.posts.concat(action.payload.posts)
  } finally {
    yield put(actions.setLoading(false));  // state.meta.loading = false;
    yield put(actions.setFetched(true));   // state.meta.fetched = true;
  }
}

posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  const ListItems = () => {
    return (
      posts.map(d => (
      <Post
        ...
        />
      ))
    )
  }

  return (
    <List 
      itemsElement={ <ListItems />}  // $$typeof: Symbol(react.element)
      ...
    />
  )
}

list.jsx

export const List = (props) => {
  
  return (
    <>
      ...
      { props.itemsElement }
      ...
    </>
  );
}


export default List

post.jsx

export const Post = (props) => {
  
  useEffect(() => {
    // This code runs every time when the state changed
  }, [])

  return (
    ...
  );
}

export default Post

以及如果我更改帖子代码像下面一样,使用效率仅运行一次。

posts.jsx

export const Posts = (props) => {


  return (
    posts.map(d => (
      <Post
        ...
      />
    ))
  )
}

如何避免这种情况?

我正在考虑立即改变状态,

setData: (state, action) => {
  state.posts = state.posts.concat(action.payload.posts)
  state.meta.loading = action.payload.loading
  state.meta.fetched = action.payload.fetched
},

但我认为也许有更好的方法。 如果您可以指出更好的方法,我会很感激。

I have a List component to control same logic for list.
And I'm passing rendered react element as a props to the List component.

But, useEffect in child component runs every time when the state changed.

Simplified code is like below:

saga

export function* fetchPosts(action) {
  try {
    const res = yield call_api(action.url);
    yield put(actions.appendPosts(res.data.posts);  // state.posts = state.posts.concat(action.payload.posts)
  } finally {
    yield put(actions.setLoading(false));  // state.meta.loading = false;
    yield put(actions.setFetched(true));   // state.meta.fetched = true;
  }
}

Posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  const ListItems = () => {
    return (
      posts.map(d => (
      <Post
        ...
        />
      ))
    )
  }

  return (
    <List 
      itemsElement={ <ListItems />}  // $typeof: Symbol(react.element)
      ...
    />
  )
}

List.jsx

export const List = (props) => {
  
  return (
    <>
      ...
      { props.itemsElement }
      ...
    </>
  );
}


export default List

Post.jsx

export const Post = (props) => {
  
  useEffect(() => {
    // This code runs every time when the state changed
  }, [])

  return (
    ...
  );
}

export default Post

And if I change Posts code like below, useEffect runs only once.

Posts.jsx

export const Posts = (props) => {


  return (
    posts.map(d => (
      <Post
        ...
      />
    ))
  )
}

How can I avoid this?

I'm thinking of changing the state at once like below,

setData: (state, action) => {
  state.posts = state.posts.concat(action.payload.posts)
  state.meta.loading = action.payload.loading
  state.meta.fetched = action.payload.fetched
},

But I think maybe there is a better way.
I'd appreciate it if you could point me to a better way.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

北方的巷 2025-02-17 00:47:55

我解决了这个问题。

在这种情况下,更新

此功能似乎更好,因为我不必使用usecallback
我没有意识到这两个是不同的,但是它们是不同的。

posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  return (
    <List 
      itemsElement={  // $typeof: Symbol(react.element), type: Symbol(react.fragment)
        <>
        {
          posts.map(d => (
            <Post
             ...
            />
          ))
        }
        </>
      }  
      ...
    />
  )
}

原始答案

我需要在usecallback中包装listItems函数。

posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  const ListItems = useCallback(() => {
    return (
      posts.map(d => (
      <Post
        ...
        />
      ))
    )
  }, [posts]); 

  return (
    <List 
      itemsElement={ <ListItems />}  // $typeof: Symbol(react.element), type: () => {…}
      ...
    />
  )
}

I solved this issue.

Update

This seems better for me in this case because I don't have to use useCallback.
I didn't realize these two were different, but they are different.

Posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  return (
    <List 
      itemsElement={  // $typeof: Symbol(react.element), type: Symbol(react.fragment)
        <>
        {
          posts.map(d => (
            <Post
             ...
            />
          ))
        }
        </>
      }  
      ...
    />
  )
}

Original Answer

I need to wrap ListItems function in useCallback.

Posts.jsx

export const Posts = (props) => {
  const { meta, posts } = useSelector((state) => state.postState);

  const ListItems = useCallback(() => {
    return (
      posts.map(d => (
      <Post
        ...
        />
      ))
    )
  }, [posts]); 

  return (
    <List 
      itemsElement={ <ListItems />}  // $typeof: Symbol(react.element), type: () => {…}
      ...
    />
  )
}

若能看破又如何 2025-02-17 00:47:55

这是您要遇到的问题:

  • 您正在定义&lt; listItems/&gt; react component 每次 posts rece> recre renders。
  • 因此,listItems的孩子&lt; post/&gt;然后被卸下并重新安装,因此效果始终运行。

相反,您应该是“在react中思考” 这将帖子作为A prop 而不是在闭合中定义它(又称您的posts component)。

每个post < / code>仍将重新渲染,但它不是完整的卸载 /重新启动,因此效果不会再运行。如果您想完全防止渲染,可以用 React.memo

这是一个简化的示例,不使用萨加斯,但假装 fetch 来生成一些“帖子”数据:

const { React, ReactDOM } = window;
const { useEffect, useState } = React;

const mockedPosts = Array(3)
  .fill()
  .map((_, i) => ({ id: Math.random(), name: `Post number ${i + 1}` }));

const fetchMockPost = (length) => {
  return new Promise((resolve) => {
    const mockPost = {
      id: Math.random(),
      name: `Post number ${length + 1}`,
    };

    setTimeout(() => {
      resolve(mockPost);
    }, 100);
  });
};

const Post = (props) => {
  useEffect(() => {
    console.log("mounted:", props);
    return () => {
      console.log("unmounted:", props);
    };
  }, []);

  return (
    <li>
      Post Name: <b>{props.name}</b>
    </li>
  );
};

const Posts = ({ posts }) => {
  return (
    <ul>
      {posts.map((post) => (
        <Post key={post.id} name={post.name} />
      ))}
    </ul>
  );
};

const App = () => {
  const [state, setState] = useState(mockedPosts);

  return (
    <div>
      <p>
        This react app passes <code>posts</code> as a <em>prop</em> so our
        component will just re-render, rather than completely unmount and mount.
        When a new post is fetched here, only the newly fetched post is{" "}
        <code>mounted</code>. The other posts <em>do</em> re-render, but their
        effect doesn't run again.
      </p>
      <button
        onClick={() => {
          fetchMockPost(state.length).then((newPost) => {
            setState((prevState) => [...prevState, newPost]);
          });
        }}
      >
        Fetch new post
      </button>
      <hr />
      <Posts posts={state} />
    </div>
  );
};

const AppThatRedefinesPosts = () => {
  const [state, setState] = useState(mockedPosts);

  const RedefinedPosts = () => {
    return (
      <ul>
        {state.map((post) => (
          <Post key={post.id} name={post.name} />
        ))}
      </ul>
    );
  };

  return (
    <div>
      <p>
        This react app always redefines <code>Posts</code> thus they always
        unmount/mount when a new post is fetched. You can see the logs show
        everything <code>unmount</code> and then everything <code>mount</code> afterward.
      </p>
      <button
        onClick={() => {
          fetchMockPost(state.length).then((newPost) => {
            setState((prevState) => [...prevState, newPost]);
          });
        }}
      >
        Fetch new post
      </button>
      <hr />
      <RedefinedPosts />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));
ReactDOM.render(<AppThatRedefinesPosts />, document.getElementById("app-original"));
#app,
#app-original {
  margin: 0.5em;
  padding: 0.5em;
  background: beige;
}

/* misc, help with scrolling with virtual console */
body {
  padding-bottom: 200px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>

<div id="app"></div>
<div id="app-original"></div>

Here is the problem you are running into:

  • You are defining the <ListItems /> React Component each time that Posts re-renders.
  • Because of that, ListItems's children <Post /> then are unmounted and remounted, hence the effect always running.

Instead, you should be "thinking in a react" and make a new component that accepts the posts as a prop rather than defining it within a closure (aka your Posts component).

Each Post will still re-render, but its not a full unmount / remount, so the effect won't run again. If you'd like to prevent the render altogether, you can memoize the component with React.memo.

Here's a simplified example that doesn't use sagas but fakes a fetch call to generate some "posts" data:

const { React, ReactDOM } = window;
const { useEffect, useState } = React;

const mockedPosts = Array(3)
  .fill()
  .map((_, i) => ({ id: Math.random(), name: `Post number ${i + 1}` }));

const fetchMockPost = (length) => {
  return new Promise((resolve) => {
    const mockPost = {
      id: Math.random(),
      name: `Post number ${length + 1}`,
    };

    setTimeout(() => {
      resolve(mockPost);
    }, 100);
  });
};

const Post = (props) => {
  useEffect(() => {
    console.log("mounted:", props);
    return () => {
      console.log("unmounted:", props);
    };
  }, []);

  return (
    <li>
      Post Name: <b>{props.name}</b>
    </li>
  );
};

const Posts = ({ posts }) => {
  return (
    <ul>
      {posts.map((post) => (
        <Post key={post.id} name={post.name} />
      ))}
    </ul>
  );
};

const App = () => {
  const [state, setState] = useState(mockedPosts);

  return (
    <div>
      <p>
        This react app passes <code>posts</code> as a <em>prop</em> so our
        component will just re-render, rather than completely unmount and mount.
        When a new post is fetched here, only the newly fetched post is{" "}
        <code>mounted</code>. The other posts <em>do</em> re-render, but their
        effect doesn't run again.
      </p>
      <button
        onClick={() => {
          fetchMockPost(state.length).then((newPost) => {
            setState((prevState) => [...prevState, newPost]);
          });
        }}
      >
        Fetch new post
      </button>
      <hr />
      <Posts posts={state} />
    </div>
  );
};

const AppThatRedefinesPosts = () => {
  const [state, setState] = useState(mockedPosts);

  const RedefinedPosts = () => {
    return (
      <ul>
        {state.map((post) => (
          <Post key={post.id} name={post.name} />
        ))}
      </ul>
    );
  };

  return (
    <div>
      <p>
        This react app always redefines <code>Posts</code> thus they always
        unmount/mount when a new post is fetched. You can see the logs show
        everything <code>unmount</code> and then everything <code>mount</code> afterward.
      </p>
      <button
        onClick={() => {
          fetchMockPost(state.length).then((newPost) => {
            setState((prevState) => [...prevState, newPost]);
          });
        }}
      >
        Fetch new post
      </button>
      <hr />
      <RedefinedPosts />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));
ReactDOM.render(<AppThatRedefinesPosts />, document.getElementById("app-original"));
#app,
#app-original {
  margin: 0.5em;
  padding: 0.5em;
  background: beige;
}

/* misc, help with scrolling with virtual console */
body {
  padding-bottom: 200px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>

<div id="app"></div>
<div id="app-original"></div>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文