Search code examples
reactjsuse-effect

How to avoid running multiple useEffect when I pass `react.element` as a props?


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.


Solution

  • 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: () => {…}
          ...
        />
      )
    }