One of the most common places a useMemo
is seen (in the wild) is for memoizing React context value. Let's look into what are the advantages of doing this and how we can prevent unnecessary renders using this pattern.
Context provides a way to pass data through the component tree without having to pass props down manually at every level. General use case for a Context is as follows:
const UserContext = React.createContext(); // could have a default value
function UserCtxProvider({ children }) {
const [authUser, setAuthUser] = React.useState(null);
const value = React.useMemo(() => ({
authUser, setAuthUser
}), [authUser]);
return (
<UserContext.Provider value={value}>
{children}
<UserContext.Provider>
);
}
function App() {
return (
<UserCtxProvider>
<Login />
<Home />
{/* Rest of the app */}
</UserCtxProvider>
);
}
By adding context provider, we are ensuring that all components rendered from App
component can access authenticated user object from context without having it to be passed down multiple levels to the bottom. The children can access it with the useContext
hook.
function Greeting() {
const { authUser } = useContext(UserContext);
return <p>Hello {authUser?.name}</p>;
}
Whenever the value in UserContext
changes, Greeting
component would automatically be re-rendered by React.
If you have used this before, you will notice a common pattern that I used in the example. The context value is passed through a useMemo
before being provided to the Provider
component. The most common advice on the Internet is that this prevents unnecessary renders.
Rather than blindly follow the pattern, we will look to analyse how useMemo
can help and learn more about how React renders components along the way. Let's look at creating an example to play around with.
We will create a context using useMemo
and then one without it so we can compare what happens with both.
We have created two CustomCtx
with providers ProviderWithMemo
and ProviderWithoutMemo
(I'm very creative with names, I know). Clicking the Increment
button increments the count by updating the context value, this will also print logs of rerendering component.
If you open React DevTools panel on codesandbox, you can check Highlight updates when components render option to visually follow along when a component renders.
From the logs, we can see that
So adding just useMemo
has not prevented any unnecessary rerenders so far.
What if there was an extra parent in between? Would the parent rerender when the context is updated? Would the extra parent rerender if main parent state is updated?
<Context.Provider value={value}>
<Parent />
</Context.Provider>
To find out why useMemo
did not prevent any rerenders, let's look at what it does in the first place. React docs describe it as follows:
Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Our usage with context does not seem to align with this definition of useMemo
. Creating an object is not an expensive operation in any way.
But since it is an object being created, but there is an advantage of using of recreating the object only when one of the fields change.
JavaScripts objects follow the rule of referential equality, which means that for them to be equal (===
, ==
or Object.is
) they need to be of the same instance.
/* Even when all fields are the same, two objects are not equal */
{} == {} // false
{ name: 'Bruce' } == { name: 'Bruce' } // false
/* If both objects share the same instance, they are equal */
const person1 = { name: 'Wayne' };
const person2 = person1;
person1 == person2 // true
/* Even if you change one of the fields */
person2.name = 'Batman';
person1 == person2 // true
In our context example, we have used useMemo
to make sure that the object instance is not changed unless the context value itself has not changed.
In fact, We are already using this very technique to force a render from the App
component. useState
compares (==
) current state with previous state and if they are different, rerenders the component. We are setting the state to {}
on clicking the button so that React would find a new instance of empty object every time it updates.
function App() {
const [, forceRender] = React.useState();
return <button onClick={() => forceRender({})}>Re-Render</button>;
}
But by default, React does not use this comparison mechanism for parent-child rerenders. Instead it rerenders all child components when the parent rerenders. How do we force it to check for equality?
We can use the higher order component memo
provided by React to check equality before rerendering a component. This works as follows:
If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
But what about the context? The child component needs to render when context value changes, right? The docs have an answer for that as well.
React.memo only checks for prop changes. If your function component wrapped in React.memo has a
useState
,useReducer
or**useContext**
Hook in its implementation, it will still rerender when state or context change.
Let's memoize our Child
component.
Now, if we pop open the console, we can see that Child
with useMemo
does not rerender when the parent component is updated.
App
updates. But the child in memoized context does not (the Child itself is a memoized component).Parent
is part of children
prop in ContextProvider
but it is not the component's child nor is it subscribed to the context.Opinion: I just leave useMemo
into most of the context values created. Other developers (or myself) might add memo
to give the app a performance boost finding something is slow later on. At that point, it's difficult to find all context states and memoize values as changing context value change might not even happen during testing phase. They seem like very unrelated concerns.
But as they say, Premature optimization is the root of all evil so please do measure before you optimise. Adding memo
to all components in the application would just add development time and not help with the performance boost.
There are other ways to optimise your React contexts: