The Subtle Aspects of React Re-rendering (Part 2): Dynamic children, memos, and constant functions

Jonathan Boccara
Doctolib
Published in
5 min readMay 26, 2021

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 modified
  • GrandParent re-renders because it is a hardcoded child of CountContextProvider which is re-rendering
  • Child re-renders because it accesses count, 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 countin 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.

--

--