Performant React Cookbook: callback props
In this article we will have a look at how to pass a callback function to React components across multiple examples, and how to optimize each case. This will prevent wasted re-renders, enable you to avoid performance bottlenecks from the beginning and make your app much faster for your users. Let’s get started! 🚀
Simple callback function
The very first case we will talk about the most common pattern we do when we need to pass a callback function to a React component.
First let’s have a look in the context of class components:
At first, everything seems to be fine here. However, this approach is going to create a new instance of the function passed inside of the onClick
prop. Since this is passing a new prop to the Button
component, it will re-render completely.
In many cases, this is not a really big issue unless your Button
component is very heavy. But this approach can cause big problems when you need to render some heavier components. There are couple of way to prevent `Button` re-rendering in this case:
- Bad way — ignore updates to the
onClick
prop (using shouldComponentUpdate, for example) and call the value which was passed during the first render. This may cause some unexpected issues down the line, when you would expect that you’re using the last version of the callback while you won’t. It may take a while to inspect the root cause for such bugs. So better be aware of it. - Good way — pass the same reference to a function on each render. This can be easily achieved:
This way only one instance of onClick
callback will be created and it will persist between re-renders. Button
will never re-render because of a changed onClick
property, as it always stays the same.
Let’s have a look at the same problem in the context of functional components as things behave a bit differently there.
You can observe the same pattern here: On each render, a new instance of the callback function is created, which is unnecessary.
The better way to avoid that inside a stateless functional component is to use the React.useCallback
hook. It will persist the callback reference unless some of its dependencies changed (we will talk about dependencies later). Here is an example how to use it:
This will reuse the onClick
function between re-renders of the Form
component.
We just went through very basic examples where you see that preventing re-rendering doesn’t take much of your time and it might save you tons of time when you need to optimize your application later.
Callback function with dependencies
In this section we are going to talk about a more complicated case when we have dependencies to props, state inside of our callback functions.
Let’s start again by looking at the example using class components:
Here we face example of the same problem as in the previous section and we can apply the same pattern we did before by defining the callback as a static method on the component class:
Let’s move on the how to resolve this problem with stateless functional components. Let’s have a look at the solution from the previous section:
As you see we have [onSubmit]
as the second argument of React.useCallback
hook. This will make sure that onFormSubmit
is updated only when onSubmit
property is changed.
Callback function with dependencies which are updated on each render
There is another case when dependencies of your callbacks are updated on each render so in order to keep your callbacks up-to-date, you need to re-create them on each render. Let’s see how we can fix that.
One possible way would be to have a pull mechanism to get the latest state of data when the callback is triggered. Here is an example for class based component:
This approach is very similar to the case when we were taking data from this.state
in one of our previous examples. So here we simply pull the data whenever we need it.
Let’s have a look at a functional component, where things will be a bit more complicated.
As you see we had to use React.useRef
in order to have a mutable source from where we can read data whenever we need it. Keep in mind that any reference to emailRef
object is always the same, so we can theoretically skip it from the list of dependencies of React.useCallback
, as it never changes.
To make things look a bit better you can use useLatestValue
hook which hides ref details under the hood and you receive a better looking API:
Here is an example using the hook in a component:
You don’t need to create refs yourself and simply get a function that returns the latest value.
The reference to this function is also static, so we can skip it from dependencies list of React.useCallback
.
Callback functions for array elements
In this section we are going to solve another callback puzzle when you need to iterate over array elements and use the data from the array inside of you callback functions which are passed to rendered components. Here is an example of how this might look in practice:
The obvious problem here is that on each re-render of the List
component, you will have all Element
components re-rendered due to the onClick
property being updated. I assume this is usually not what we want. There are multiple ways of solving this problem, but we will have a look at the most optimized one:
The main idea is that you should adjust the logic of Element
component a little bit, so that it passes the data
property (or any other) to our callback property. This enables us to have a static onClick
callback and the data will arrive there through its arguments. Here’s an example:
This way if we have any update prevention mechanism (e.g. React.memo or React.PureComponent), Element
won’t be updated as on each re-render of List
component we will receive the same properties (references to objects will be the same). It may save you quite a lot of time if you implement everything in an optimized way from the very beginning.
Let’s have a look at another use case. So you have this.props.onElementClicked
inside the List
component and you need to pass there not just element data, but also the index of the item which was clicked. This may sound challenging, however we can still solve it with one single line of code. You may think for a little while and then compare your ideas with solution example:
That would be one solution, however it’s not optimal for very big collections of data (5–10k+ elements). To improve the performance for big collections of data, and still be able to get an index, you may need to pass the index
property to the `Element` when rendering and then pass it to onClick
property. This can be done using an object or a separate argument.
A sample implementation could be:
This way all 3 properties of the Element
component are staying the same between re-renders and your onElementClicked
callback is receiving the data based on which element was clicked.
Keep in mind that stateless functional components don’t prevent updates in case parent re-renders. Even when the same properties are passed functional components will update, to prevent that you should wrap them with
React.memo
Let’s apply all principles which we saw above to a stateless functional component so we know how to make it optimized as well as it might be not that obvious. Let’s start from this implementation of List
component:
Let’s apply the knowledge we gained from previous examples to solve this case for stateless functional component rendering a list of elements. Solution will looks like that:
As you see it’s exactly the same approach we had before for class components, so we had to adjust a bit the behaviour onClick
callback inside of Element
component. If you need to extend the onElementClicked
call with some more data like index, you can apply the same approach as we did in the previous example with class List
component.
What if you cannot change Element component?
In this section we will solve the same problem as in the previous section, with the only difference that we are not allowed to modify the Element
component (considering it could be node module). One possible way would be to memoize the callback functions you pass to Element
components so that you always pass the same references between re-renders. This is how the solution would look like for class List
component:
In the example above, the memoized
function returns the same reference to a callback function (elementData) => …
whenever you pass the same index
again. On the first render of List
component it will create callbacks for each index
, but on the second one it will return you memoized callbacks created during the first render. More about the concept of memoization can be found in the lodash.memoize()
docs or any other utility library.
However, this approach becomes very memory consuming if you have a lot of elements in your list, so we will try and improve that:
The main idea here is that we store callbacks for each index inside of the WeakMap
object. The main difference to Map
is that once nothing is referencing the callback anymore, it will be removed from memory. This allows to avoid memory leaks.
With the current implementation of the render
function, all callbacks will be references all the time so it won’t give you a big benefit on a large collection of elements. But, if you apply a virtualized principle when rendering a list of elements, you’ll gain all benefits of callbacks being removed from memory for elements which are not rendered. Here are couple of resources about virtualized approach for rendering big lists of data:
- https://bvaughn.github.io/react-virtualized
- https://github.com/tannerlinsley/react-virtual
- https://web.dev/virtualize-long-lists-react-window
- https://blog.logrocket.com/rendering-large-lists-with-react-virtualized-82741907a6b3
Some words about profiling
Profiling is another very important topic which deserves a separate article. But the most basic thing you can do is to check if your app have problems with wasted renders using the great why-did-you-render library.
If that’s not enough you can try out the timeline tool in Chrome devtools as shown in this article.
If you want to see an impact of unoptimized rendering you can play around with this sample project from Kent C. Dodds.
Takeaways
I hope you enjoyed reading and learned some small tricks on how to overcome the problem when you need to pass dynamic callbacks into your components. Our experience showed it’s easier and faster to keep the app in the optimized state than refactoring everything when the app got too slow.