Redundant re-renders are a common issue in React. If not taken seriously, this issue can quickly worsen the performance of your application.
By understanding and implementing these practices, you can avoid the problem and keep your rendering process running smoothly.
memo and useCallback
Let’s first cover the main toolkit under your belt for cutting down the re-renders:
useCallback. You should know how these utility functions work in order to be able to optimize React performance.
is a utility method that React library provides to memoize the state of the components and make it so that they only render when their props change. All you need to do is wrap your components declaration with the function:
ListItem component will no longer re-render when its parent does. The only time the
ListItem would re-render is if the props passed to it change. This is especially useful when working with large lists. List items will not re-render each time the state of the parent list changes. This tweak could drastically improve the performance of your UI.
So what about
? In the previous example, our
ListItem did not receive any event handler props. Yet passing event handlers from parent to child is a common pattern that you will, no doubt, use a lot when working with React.
There is, however, a slight problem with passing event handlers to memoized components. Event handlers are usually declared inside of the parent component, which means they will re-render each time the parent component does. This will make wrapping our child components with
memo useless since the event handler prop will cause it re-render each time anyway.
And that’s where
useCallback comes in. This utility method returns a memoized version of any callback function it receives. The only time it would recalculate the value of the function you pass is if any of the dependencies you provided change.
Let’s see how we can use
useCallback to pass an event handler to our
ListItem without causing redundant re-renders:
In the example above, we wrapped the
handleClick event handler with
useCallback will memoize our handler function after the initial render. Next time our parent list re-renders,
useCallback will return the same memoized version of our handler function. The
ListItem component will thus avoid a redundant re-rendering since none of its props changed.
Note: Whenever you use
memo to reduce re-renders for a component, be sure to also wrap the event handler properties that the component receives with
useCallback. Otherwise, your efforts are futile.
Be discriminating about what your components get to decide
Whenever you create a React component, keep in mind the scope of the context you’re giving it. Ask yourself if the component should really be making decisions about certain things.
For example, it’s rare for components to need to decide if they should render themselves. It’s usually better for the parent components to make that decision.
When rendering lists, err on the side of providing strictly the props the list items need to function. Try to avoid passing any of the state variables of the parent to the child. Passing parent’s state variables will cause ALL of the list items to re-render each time that variable changes. The negative effect of this is amplified as the size of your list grows.
Instead, try to derive a primitive prop based on the updated state of the parent. This way, the child components only need to re-render when the primitive changes.
Let’s take a look at an example:
Here we have the
ListItem for displaying items inside the
title properties. We’re also outputting the “Rendered” word to the console each time the
ListItem renders to see how many times it rendered. Note that we’re also wrapping our
memo to reduce the number of re-renders for each item.
For the sake of the example, we’re setting
2 inside the
useEffect. Doing so will trigger another re-render of the parent component after the initial render.
After running the example above, the console will output the word “Render” a total of 6 times: 3 times during the initial render of each list item and 3 more times when the
setSelectedItemId is called.
Now let’s see how we can avoid the redundant renders:
In this example,
ListItem receives the
isSelected prop that lets it know whether the item is selected. Now it’s up to the parent
List component to make that decision. As a result, the word “Rendered” will only be printed 4 times. That’s because the items that weren’t selected didn’t re-render since none of their props changed.
While cutting down from 6 to 4 renders might not seem like a big deal, when you’re working with large lists that render a lot of data and have sizeable component trees, you can significantly improve their performance using this approach.
Use flattened props and primitives
This tip relates to the previous one. When passing complex data structures as props, try to pass flattened and simplified versions instead of the whole objects.
Better yet, try to extract the fields your component needs from the data structure as primitive props. This will make it easier to optimize the performance of your components in the future.
Let me show you what I mean. Say we have a
Profile component that displays user information:
Nothing particularly terrible about this component. But, we should ask ourselves: “Do we really need the whole pass the whole user object?”
Since we’re only using two fields from the object, we could accept them as primitive props instead:
user object got re-created, which can easily happen multiple times within the component’s lifetime, this will not cause re-renders for our
Profile doesn’t care if the
user object gets recreated, as long as the values of the
name fields stay the same. Now it’s much easier to optimize
Profile by wrapping it with
Be cautious with useEffect
is how you tap into the lifecycle of the component. However, I often see it being used in places it could’ve been avoided. The main issue I see with overusing
useEffect is that your app will be full of unintended side effects that might cause redundant re-renders or, worse, redundant server calls.
Let’s look at an example. Say we have a
UserList component that displays user profiles. You can expand each profile to view more info:
In the code above, we’re using
useEffect to track when we should be fetching additional info but is that necessary? A simpler and more future-proof solution is to fetch additional information inside of the expand click handler:
The main reason this is better is that as the complexity of your component grows, you might introduce other pieces of logic that will change
selectedUserId but that don’t necessarily need to request additional info. If you’re not careful, you might trigger requests in cases where you didn’t intend to.
useEffect is justified and might even be the only viable solution. With that said, I suggest double checking if you need
useEffect. Sometimes you can get away with relying on other, safer triggers, such as user actions.
In this post, we looked at some of the practices for optimizing your React rendering process. By following these tips, you can avoid unnecessary re-renderings and improve the performance of your app. I hope you find them useful!
If you would like more tips on writing better React code, check out my other post “ Simple tips for writing clean React components ”.
If you’d like to get more web development, React and TypeScript tips consider
following me on Twitter,
where I share things as I learn them.