Skip to content

Performance

In the last module, we learned how to create “pure” components with React.memo. A pure component is one that doesn't re-render unless its props or state changes.

But what happens when a pure component consumes a context? For example:

import { FavouriteColorContext } from './App';
function Sidebar() {
const favouriteColor = React.useContext(FavouriteColorContext);
return (
<div style={{ backgroundColor: favouriteColor }}>
Sidebar
</div>
)
}
export default React.memo(Sidebar);

By wrapping Sidebar with React.memo, we produce a pure component, but what effect does that have? When will this component re-render?

Essentially, you can think of context as “internal props”. It follows all the same rules as props. If the value held in context changes, this component will re-render.

It's functionally equivalent to this:

function Sidebar({ favouriteColor }) {
return (
<div style={{ backgroundColor: favouriteColor }}>
Sidebar
</div>
)
}
export default React.memo(Sidebar);

So, here's our updated definition for how pure components work:

A pure component will re-render if a prop, state variable, or context value changes.

Memoizing context values

In 90% of situations, we won't be passing a single value through context. We'll pass several things, packaged up in an object.

Here's a more realistic example:

export const FavouriteColorContext = React.createContext();
function App() {
const [
favouriteColor,
setFavouriteColor
] = React.useState('#EBDEFB');
return (
<FavouriteColorContext.Provider
value={{ favouriteColor, setFavouriteColor }}
>
<Home />
</FavouriteColorContext.Provider>
);
}

We're passing the state value, favouriteColor, as well as the state-setter function, setFavouriteColor.

When we pass multiple values like this, it tends to on our pure components.

Let's solve a mystery. Below, you'll find a playground with a ColorPicker component that consumes the FavouriteColor context. There's also an unrelated piece of state, count.

ColorPicker is a pure component, and it doesn't depend at all on the count variable, but it re-renders when count changes.

Your mission is to figure out why this happens, and to fix it, so that ColorPicker only re-renders when the favouriteColor state changes.

Acceptance Criteria:

  • Clicking the “Count: 0” button should not cause the ColorPicker component to re-render.
  • A “ColorPicker rendered!” message is logged whenever ColorPicker re-renders, and so you'll know you've succeeded once clicking the “Count” button doesn't spawn a console message.

Code Playground

import React from 'react';

import FavouriteColorProvider from './FavouriteColorProvider';
import Counter from './Counter';
import ColorPicker from './ColorPicker';

function App() {
const [count, setCount] = React.useState(0);

return (
<>
<FavouriteColorProvider>
<Counter count={count} setCount={setCount} />
<ColorPicker />
</FavouriteColorProvider>
<p>
Current count: {count}
</p>
</>
);
}

export default App;
preview
console
  1. ColorPicker rendered!
  2. ColorPicker rendered!

Let's dig into this!

Note: In the video below, you might notice that there is no <p> showing the current count. I added this paragraph after recording the video, to make clear that the state shouldn't be moved into a child component. Sorry for any confusion!

Video Summary

To briefly explain what's going on here: we have an application with two pieces of state.

  • count lives in App and is exclusively used by Counter.
  • favouriteColor lives in our FavouriteColorProvider provider component, and is passed along with its setter function through context.

The ColorPicker component consumes this context, and uses it to control a color input.

ColorPicker is also a pure component, thanks to React.memo. This means that it'll only re-render when its props, state, or context changes.

And yet, this component is re-rendering when the count state variable changes! This is bewildering because neither ColorPicker nor FavouriteColorProvider depend on count.

Here's the key: the context value itself is an object with two key/value pairs. We can see this if we split it out:

function FavouriteColorProvider({ children }) {
const [favouriteColor, setFavouriteColor] = React.useState(
'#EBDEFB'
);
// This is the object getting passed through context:
const value = { favouriteColor, setFavouriteColor };
return (
<FavouriteColorContext.Provider value={value}>
{children}
</FavouriteColorContext.Provider>
);
}

Whenever count changes, the App component re-renders, which re-renders FavouriteColorProvider. This generates a brand new value object.

ColorPicker receives this value object through context, and it re-renders when this object changes:

function ColorPicker() {
const id = React.useId();
// This component re-renders when `value` changes:
const value = React.useContext(FavouriteColorContext);
// We destructure the values we need from this `value` object:
const { favouriteColor, setFavouriteColor } = value;
// The rest omitted for brevity
}

I mentioned above that context is like “internal props”. But it's the overall object, not the individual values inside that object, that are considered in pure component calculations.

// It's equivalent to this:
function ColorPicker({ value }) {
const id = React.useId();
// We destructure the values we need from this `value` prop:
const { favouriteColor, setFavouriteColor } = value;
// The rest omitted for brevity
}

So, how do we fix this? React.useMemo can help!

Here's the solution:

// FavouriteColorProvider.js
function FavouriteColorProvider({ children }) {
const [favouriteColor, setFavouriteColor] = React.useState(
'#EBDEFB'
);
const value = React.useMemo(() => {
return { favouriteColor, setFavouriteColor };
}, [favouriteColor]);
return (
<FavouriteColorContext.Provider value={value}>
{children}
</FavouriteColorContext.Provider>
);
}

We're telling React to store a reference to this value object. This object will only be re-generated if favouriteColor changes, thanks to the dependency array.

If count changes, it does cause FavouriteColorProvider to re-render, but useMemo will ignore this render, passing along the stored reference instead.

As a result, ColorPicker receives the exact same reference, and does not re-render. It'll only re-render when the context value changes, which only happens when favouriteColor is set to a new value.

This stuff is confusing. The memoization stuff is already complicated and hard to understand, and it gets even worse with context.

The intuition will come with time, as you get more practice using context. In the meantime, here's a rule of thumb you can use as a shortcut: When passing an object or array through context, always memoize it with useMemo.

Memoizing the provider component?

One of the more common ideas, when trying to solve the mystery above, is to memoize the FavouriteColorProvider component, with React.memo.

This approach turns out to be pretty counter-intuitive. Let's discuss.

Video Summary

Instead of memoizing the value to be passed through context, what if we memoize the component, like this?

function FavouriteColorProvider({ children }) {
const [favouriteColor, setFavouriteColor] = React.useState(
'#EBDEFB'
);
// Don't memoize the `value`...
const value = { favouriteColor, setFavouriteColor };
return (
<FavouriteColorContext.Provider value={value}>
{children}
</FavouriteColorContext.Provider>
);
}
// ...memoize the entire component!
export default React.memo(FavouriteColorProvider);

This seems like it should work! But the problem is that children prop.

As I've mentioned before, we tend to think that children is a special thing, but really it's a prop like any other.

Inside App, we see how children is defined:

function App() {
const [count, setCount] = React.useState(0);
return (
<FavouriteColorProvider>
<Counter count={count} setCount={setCount} />
<ColorPicker />
</FavouriteColorProvider>
);
}

The JSX obfuscates this a bit, but let's take that <Counter> element as an example.

In pure JS, this would be:

React.createElement(Counter, { count, setCount });

When we render this component, the createElement function returns a “React element”, which is really a JS object:

{
$$typeof: Symbol(react.element),
type: ƒ Counter,
props: { count, setCount },
},

Because FavouriteColorProvider has two child elements, the actual children prop is an array, like this:

[
{
$$typeof: Symbol(react.element),
type: ƒ Counter,
props: { count, setCount },
},
{
$$typeof: Symbol(react.element),
type: ƒ ColorPicker,
props: {},
},
]

Here's the catch: Every time App renders, we re-run all this code, generating a brand-new array containing two brand-new elements.

If a React component takes a React element as a prop (often children, but as we learned in the “Slots” lesson, it can be any prop), that React element will be regenerated on every render.

Can we memoize a React element? What if we do something like this?

function App() {
const [count, setCount] = React.useState(0);
const counterElem = React.useMemo(() => {
return <Counter count={count} setCount={setCount} />
}, [count]);
console.log(count)
return (
<FavouriteColorProvider>
{counterElem}
<ColorPicker />
</FavouriteColorProvider>
);
}

This is a weird idea, and not something I've ever seen anyone try.

It also doesn't work (at least, not in this case), because the element will be regenerated whenever count changes – it's needed by the <Counter> element!

This stuff is funky as heck, but here's the takeaway: When it comes to context providers, we should memoize the value. We can't memoize the component itself.

Here's the sandbox with our attempted solution, if you'd like to poke around:

Code Playground

import React from 'react';

import FavouriteColorProvider from './FavouriteColorProvider';
import Counter from './Counter';
import ColorPicker from './ColorPicker';

function App() {
const [count, setCount] = React.useState(0);

const counterElem = React.useMemo(() => {
return <Counter count={count} setCount={setCount} />;
}, [count]);

return (
<FavouriteColorProvider>
{counterElem}
<ColorPicker />
</FavouriteColorProvider>
);
}

export default App;
preview
console
  1. ColorPicker rendered!
  2. ColorPicker rendered!