Don't Surrender to Extraneous React Re-Renders
Mark J. Lehman
Posted on November 7, 2019
After learning about and monkeying around with this fantastic React tool why-did-you-render
for about a week, I realized there was a lot that I didn't realize or understand about how React determines when to re-render a component. Here are 6 of the most helpful things I learned during this adventure.
1. Use React.memo for pure functional components
With React hooks, it's easier than ever to use functional components rather than class components. Larger and/or more complex components can be written as functions instead of classes. However, vanilla functional components re-render with every change to props, and when dealing with a large or complex component, that might not be necessary.
Enter React.memo
. This makes a functional component behave similar to extending React.PureComponent
-- namely, that it will do a shallow comparison of props on any prop change, and only re-render if previous props shallowly equal new props.
2. Pass in a comparison function for doing deep compares
Shallow comparison might not do the trick though. After all, maybe one of the props is an array of strings. If that array is generated on the fly somehow, for example by taking something from state and using map
or filter
to get only certain ones, even if the array contents hasn't changed, the new prop will be a new array, so prevProps.arrayProp === this.props.arrayProp
will be false, and the component will re-render unnecessarily.
Luckily, React.memo
takes a second argument that will be used to compare the props. So if there are limited props that can be compared deeply, that can avoid some unnecessary re-renders. A few notes about this:
- The docs say this is not guaranteed to prevent re-renders. However, anecdotally I have noticed fewer re-renders using this approach.
- Depending on how large or "heavy" the component is, and depending on how complex the props are, it's a good idea to determine whether it will be more performant to re-render or do a deep compare.
- This is more or less analogous to the
shouldComponentUpdate
lifecycle method onReact.Component
, only in reverse (e.g. ifshouldComponentUpdate
returned true, the component would re-render; whereas if this passed-in functionareEqual
returns true, the component does not re-render.)
3. Only update state if it has changed
As you can see, the name of the game in reducing re-renders in general is to avoid props changes. Sometimes that will mean adding a bit more complexity elsewhere. For example, on our team, we like simple cases in reducers, such as this:
case 'DOMAIN/UPDATE_ARRAY_PROP': {
const { propName, arrayProp } = action;
return Object.assign({}, state, {
...state,
[propName]: arrayProp
})
}
But, if state[propName]
is deeply equal to arrayProp
, we are reassigning that property even though it isn't actually changing. And as we just learned, reassigning the property, particularly when dealing with array and object props, creates a new array or object which will cause shallow comparisons to fail.
Instead, we should check if an UPDATE action is actually going to update, or if the updated values are the same as what's currently in state. If they are the same, don't update them and return state as-is to avoid the re-render. The above example, reworked (using lodash/isEqual
):
case 'DOMAIN/UPDATE_ARRAY_PROP': {
const { propName, arrayProp } = action;
// Add this guard!
if (isEqual(state[propName], arrayProp)) return state;
return Object.assign({}, state, {
...state,
[propName]: arrayProp
})
}
To further illustrate this, here's an example updating an object's property.
With extraneous re-renders:
case 'DOMAIN/UPDATE_OBJECT_NAME': {
const { objectName, newName } = action;
return Object.assign({}, state, {
...state,
[objectName]: {
...state[objectName],
name: newName
}
})
}
Optimized:
case 'DOMAIN/UPDATE_OBJECT_NAME': {
const { objectName, newName } = action;
// Add this guard!
if (state[objectName].name === newName) return state;
return Object.assign({}, state, {
...state,
[objectName]: {
...state[objectName],
name: newName
}
})
}
4. Avoid data conversion in selectors
Same problem, different symptom. When using selectors, avoid doing any data conversion if possible. This includes using map
and filter
. I have experience with selectors becoming a kind of repository of helper functions that do a lot of map
ping and filter
ing. Using tools like reselect
can help with this by memoizing the return values of the selectors.
Even so, some selectors might be better moved to helper functions, imported into the functions, and used to map
or filter
values pulled directly from state. Because a selector that pulls from state and then map
s or filter
s will return a new array and re-render every time, whereas using a helper function in the component would have the component only re-render when that value in state has changed.
5. Get only what is needed from state
In selectors, fetch only what is needed in the component. For example, if I only want to check the count of some array of objects, I don't want to load the whole array into props, I just load the count for simpler shallow comparison.
6. No anonymous functions as props
I have seen and done this many times before realizing it was problematic:
<SomeComponent
onError={() => console.error('BAD')}
/>
Every render of SomeComponent
will compare that function against its previous iteration, and since it's anonymous, it will be effectively a different function each time, resulting in shallow prop comparison failure.
Instead, define functions outside the component and then pass in the named function:
const logError = () => console.error('BAD');
<SomeComponent
onError={logError}
/>
There are also some more complicated and helpful examples in the issue tracker for why-did-you-render.
Conclusion
Remember that React itself seems generally very performant, so it's important to try not to get bogged down in wiping out all unnecessary re-renders. With small enough components, even if they re-render all the time, it likely won't have noticeable affects on app performance. For me, I choose to focus on the big heavy component re-renders and any low-hanging fruit for the smaller components, and I don't sweat the other stuff.
Image credit Louis Hansel
Posted on November 7, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.