How to avoid re-render of children components

Viewed 1223

I have a rendering problem that I can't fix. On my application, I have a Posts component that has a state containing an array of posts data.

[
  { id: "P01", title: "First post", likeCount: 0 },
  { id: "P02", title: "Second post", likeCount: 0 }
]

In this component, I map the posts state to return a Post component for each post inside the posts state.

Posts component

export default function Posts() {
  const [posts, setPosts] = useState(null);

  const fetchPosts = () => {
    setPosts([
      { id: "P01", title: "First post", likeCount: 0 },
      { id: "P02", title: "Second post", likeCount: 0 }
    ]);
  };

  const handleLike = (id) => {
    setPosts(
      posts.map((post) =>
        post.id === id ? { ...post, likeCount: post.likeCount + 1 } : post
      )
    );
  };

  return (
    <div>
      <button onClick={fetchPosts}>Fetch posts</button>
      {posts !== null ? (
        posts.map((post, index) => {
          return <Post key={index} post={post} handleLike={handleLike} />;
        })
      ) : (
        <p>No post to dipslay</p>
      )}
    </div>
  );
}

Post component

function Post({ post, handleLike }) {
  console.count("Renders for: " + post.id);
  return (
    <div>
      <h1>{post.title}</h1>
      <p>Like count: {post.likeCount}</p>
      <button onClick={() => handleLike(post.id)}>Like</button>
    </div>
  );
}


export default React.memo(Post);

The problem is that, whenever I like a Post, all the other Post components re-render. I added a console.count() on the children component Post to show this in action (see below for the sandbox link). Because of that, I have a performance issue because, on my appplication, I have a lot of posts to display with a lot of children components.

I tried to wrap the Post component using React.memo but it is still re-rendering the component because the handleLike function passed as props to the component also changed when Posts re-rendered. Maybe it is possible to avoid this rendering problem using the useMemo / useCallback hooks, but I'm not familiar with those and i can't get them to work as I want to.

What are my options here?

You can find a simplified demo code here: https://codesandbox.io/s/re-render-issue-esm01

7 Answers

Never use indexes as key as per react's official document. Use of index as key in react component lead you to unwanted re-rendering.

Let me explain in brief. A key is the only thing React uses to identify DOM elements. What happens if you push an item to the list or remove something in the middle? It will change all the keys of other component as well and re-render them.

For improving more you can use React.memo and React.useCallback. You can find more details in this link

I would like to highlight the below points before anything else

When a component’s props or state change, React decides whether an actual DOM update is necessary by comparing the newly returned element with the previously rendered one. When they are not equal, React will update the DOM.

Even though React only updates the changed DOM nodes, re-rendering still takes some time. In many cases it’s not a problem, but if the slowdown is noticeable, you can speed all of this up by overriding the lifecycle function shouldComponentUpdate (React.memo if you are using function component)

Coming to your example, I have inspected the DOM and observed that only the respective like count is updating in actual DOM.

DOM update

To combat with performance issue (noticeable slowdown in re-rendering), you can use approach suggested in Jayshree Wagh's answer as below

// Posts.js
const handleLike = useCallback(id => {
  setPosts(posts => posts.map(
      post => post.id === id ? ({ ...post, likeCount: post.likeCount + 1 }) : post
    )
  );
}, []);

// Post.js
export default React.memo(Post, (prevProps, nextProps) => prevProps.post === nextProps.post);

See the good example for useCallback.

Avoid using index as key in case you are using it in actual code.

You can do something like this

const Post = React.memo(
  props => {...},
 (prevProps, nextProps) => prevProps.post === nextProps.post
);

when the function returns true, the component will not be re-rendered

You should be using useCallback on your handleLike handler. The state setter should then be using a callback instead of a dependable value, like this:

const handleLike = useCallback((id) => {
  setPosts(
    posts=>{
      return posts && posts.map((post) =>
        post.id === id ? { ...post, likeCount: post.likeCount + 1 } : post,
      )
    },
  );
},[setPosts]);

I don't know how to do it with React Hook but you can transform Post component from functional component to class component, then use shouldComponentUpdate() to choose what make your component re-render.

I would add something like InitializeApp in a global state and an ActionCreator that you can call on your component, or you can use React.memo "https://reactjs.org/docs/react-api.html#reactmemo".

Related