I believed React is smart enough to know this…. but aye?
We had a bunch of large component trees in our company’s web application. And I saw them re-rendering from the top to deep down to the bottom for no reason. At first, I thought it would be easy to get them correct.
Of course, the tree was much bigger than this. The tree didn’t just reach the end. This made useless re-rendering even a bigger concern. From the top to bottom,
ComponentB, .... and so on.
And I saw that there were no use of
memo in our app yet. So I decided to make use of them, still not yet aware of what was to come for me.
Takeaway 1: Parent component pretty much does not care about its
I was optimizing the app and witnessed an interesting fact. Here’s the code to demonstrate it:
And do you think
Child2 are going to re-render upon
mouseEnter? See what happens:
Oops. They re-render. But I do not see a reason why (The same thing happens for
class components, if you wonder. You can try that!). The children do not even have their own props or states to listen for any changes. How can this happen? This is from React's own documentation:
shouldComponentUpdate()is invoked before rendering when new props or state are being received. Defaults to true.
It defaults to true! What… Ok. React is made that way.
Then how can we make it better?
Takeaway 2: use the
Ok. The only difference is that we use children prop.
And the result?
It does not re-render the children components anymore! I still have not understood exactly why React can in this code but cannot in the previous code, but this is what it is.
Takeaway 3: use
memo literally memoizes the functional component receiving a certain
props, and if the props stay the same, it will just return the previously rendered (same) component. The result? Same as solution 1. It does not re-render
Child2. So I'm just gonna copy and paste the gif from above for those who are lazy to read what's written, just like me:
Ok. But if you are using passing in nested objects as props, you should implement your own
areEqual function for
memo, like this:
Yeah. I know it’s an ol’ school style to check nested object equality, but it works well. In practice, you use something like
react-fast-compare that would handle nested object comparison for you.
Ok. Now, what’s the result?
Child1 no longer knows
obj prop stays the same because
memo only runs a shallow comparison between
Child2 exactly knows, by using
areEqual function, that
obj is actually staying the same and therefore it does not need to re-render itself.
Takeaway 4: Use
Of course, you might wanna use your class component. So here’s a brief explanation on that as well:
Above is an exact replica of the functional component implementations. Here are the details:
- You use
Child1, but it won't serve its purpose, just like
memo, because it has a nested object as its prop. It will keep re-rendering upon its parent component re-rendering. It would work if there were no nested object properties.
- You use
Child2and it will work. It compares nested objects just like
areEqual. In case there is a
state, you can use
nextStateto compare with
- It’s just so annoying to write lots of boilerplate of codes for a
classcomponent. That's why I prefer functional components... (yeah I digressed a bit)
Of course, the result is the same as the last code snippet. Let’s again copy and paste the same gif for the people:
Takeaway 5: Deal with functions from props when comparing
There are many cases where props are functions, especially when you plug your functions into components with redux’s
mapDispatchToProps, or passing your functions as props from parent down to children to get notified of what happened in children.
Dealing with functions might sound like really a basic thing, but it can really harm your application’s performance if you don’t do.
We see lots of patterns like this:
- Now, the
objprop no longer an object. It's just become
- You are passing a function called
- You shallowly compare if all props are the same, both in
Child2, albeit using different ways (
Child1is not using a custom
Child2is. But essentially they are doing the same thing in this context).
- Everything is going to re-render although props to
Child2are staying exactly the same. Look at the result:
But why!? Well..
The only way to make it possible is to have the same reference:
Then how can we deal with functions in props? Well, there are two ways:
- Exclude them
- Stringify them
1. Exclude them
You can use lodash’s
_.isFunction to check if something is a function. Then you only have object properties that are not functions, to apply that to
Now, you can do this:
Result? Only parent component re-renders. Essentially, you are only comparing props which are not functions.
2. Stringify them
JSON.stringify does not support stringifying functions. So you've gotta use third party libaries like jsonfn.
Let’s do this:
JSONfn.stringify will generate a string looking like:
Using this function will give you the same result as the last example. Here’s another copy and paste:
Yeap. That’s all.
- If you do not use
childrenprop, every component inside the
renderfunction of a parent will re-render by default.
- There are ways to prevent useless re-renders: (1) Use
areEqual, or (2) Use
- If there are functions in your props, before using
shouldComponentUpdate: (1) Exclude them, or (2) Stringify them
Originally published at https://9oelm.github.io/2019-10-02--Making-stupid-react-smart-in-re-rendering/.
First of all, thank you for the insightful comments.
- The cost of stringifying the functions may be larger than the benefit of optimization itself. So don’t use
- You should instead use
useCallbackin your functional component (see the comment section for an example). But still, I’m not sure how we could do that for a
classcomponent. Anybody got a good idea?