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: memo
and useCallback
. You should know how these utility functions work in order to be able to optimize React performance.
memo
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:
Now, the 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
useCallback
? 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
. 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 List
. ListItem
accepts id
, selectedId
and 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 ListItem
with memo
to reduce the number of re-renders for each item.
For the sake of the example, we’re setting selectedItemId
to 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:
If the 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
component. Profile
doesn’t care if the user
object gets recreated, as long as the values of the title
and name
fields stay the same. Now it’s much easier to optimize Profile
by wrapping it with memo
.
Be cautious with useEffect
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.
Often using 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.
Conclusion
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.
Happy coding!