The Subtle Aspects of React Re-rendering (Part 2): Dynamic children, memos, and constant functions
In the first post of our series on the subtle aspects of re-rendering in React, we saw how React behaves with function calls and lazy evaluation.
To dive deeper in React re-rendering we’ll see now how dynamic children, memos and constant functions impact rendering.
Understanding these aspects of React will make you reap performance gains for your website, stay away from a whole class of bugs, and get a deeper understanding of React.
To understand the following examples, you need to be familiar with React contexts.
Re-rendering in a hierarchy
Consider the following code:
Context
contains a CountContextProvider
component and passes it one child, the Parent
component.
Here is CountContextProvider
:
CountContextProvider
puts a GrandParent
inside of a React context providing the count
state, and passes it CountContextProvider
's children (here, Parent
).
Let’s look at GrandParent
:
GrandParent
is a component containing the child it received. In our case this child is Parent
:
Parent
returns the Child
component:
Child
contains a button that applies the identify function on the count
state coming from CountContextProvider
all the way up.
Which components re-render when we click on this button? None!
Indeed, React recognizes that the function didn’t change the state’s value, and knows that there is nothing to re-render. Let’s make the function change the state:
Now which components re-render when we click on this button?
When we click the button, CountContextProvider
, GrandParent
and Child
re-render:
CountContextProvider
re-renders because its state,count
, was modifiedGrandParent
re-renders because it is a hardcoded child ofCountContextProvider
which is re-renderingChild
re-renders because it accessescount
, a modified state
These are the reasons that can make a component re-render: having its state (or props) changed, accessing a state coming from a context that changed, or being a hardcoded child of a component that re-renders.
However Parent
does not re-render because it doesn't access count
and it is a dynamic child of CountContextProvider
: it was passed in its {children}
property, as opposed to being hardcoded like GrandParent
was.
Re-rendering of memos
Now let’s introduce a memo component in CountContextProvider
:
Here is the code of our MemoComponent
:
React.memo
indicates to React that this is a pure component, meaning that it doesn't have or access state. As a result, React only re-renders it when its props change.
Now if we go back to our Child
component and click its button, will our MemoComponent
re-render?
The answer is that it does re-render. The reason is that its prop onClick
has changed. Indeed, its source onClickMemo
is redefined whenever CountContextProvider
re-renders, and as we saw in the first example CountContextProvider
does re-render when we click on Child
's button.
To benefit from React.memo
's effect and avoid re-rendering of MemoComponent
here, we can use useCallback
to keep the same function onClickMemo
across all renderings of CountContextProvider
:
useCallback
takes two parameters: a function and a list of objects. When the component re-renders, useCallback
returns the same instance of the function as in the previous rendering, unless one of the objects of the list has changed. Those objects in the list are called dependencies.
In our case, the list of dependencies is empty, so onClickMemo
is constant across all re-renderings.
Now MemoComponent
no longer re-renders when we click on Child
's button, because its prop onClick
coming from onClickMemo
hasn't changed.
Constant functions and references
Note that in our last example with useCallback
, we had to introduce the following lines of code in CountContextProvider
:
and we used countRef
instead of count
in onClickMemo
.
If we had kept using count
, like this:
then we would have a bug! Since we keep the first instance of onClickMemo
at every render, it would come with the first value of count
in its closure. So at every render, the count
used inside of onClickMemo
would stay at 0, even though the new count
coming from the context would be incremented.
References allow to work around this problem. A reference (here countRef
) is a intermediary object pointing to a value (here countRef.current
). The reference is constant at every render, meaning that each render uses the same reference, but nothing prevents the value it points to from changing.
So at every render we update countRef.current
with the latest value of count
, and even though onClickMemo
always uses the same countRef
, it can access the latest value of count
via its .current
key.
References and useEffect
useEffect
is another common application of this principle, where we need to be careful and use references to have values up to date.
To illustrate the point, let's twist the code of ourMemoComponent
to make it use useEffect
. The resulting code is a contrived example as you wouldn’t create a button like this, but it’s only to expose the issue with useEffect
if it creates something used later:
This code creates a button just as in the previous version of MemoComponent
, but this time it associates its callback in a useEffect
function, that is called only once because it has an empty list of dependencies.
This implies that the button of MemoComponent
keeps the same version of onClick
every time it re-renders (whether we define onClickDemo
with useCallback
or not).
If we use a countRef
reference in onClickMemo
then clicking MemoComponent
's button correctly displays the latest value of count
.
But if we use count
directly in onClickMemo
, only the first value of count
, gets closed over in onClickDemo
and only that first version of onClickDemo
gets closed over in the function registered by useEffect
. As a result the button will display this first value at all re-renders.
This is why it is important to use a reference and not a value in useEffect
when it creates something that will be used in re-rendering (for example a callback).
React re-rendering
Re-rendering is at the heart of React, and a good understanding of it is essential for front-end and full stack developers.
This series on the subtle aspects of React re-rendering showed you how React behaves with function calls, lazy evaluation, dynamic children, contexts, memos and constant functions.
Those aspects are key to work in synergy with React, and have a fast website and with a correct behaviour.
If you want more technical news, follow our journey through our docto-tech-life newsletter.
And if you want to join us in scaling a high traffic website and transforming the healthcare system, we are hiring talented developers to grow our tech and product team in France and Germany, feel free to have a look at the open positions.