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:
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
- ColorPicker rendered!
- 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 inApp
and is exclusively used byCounter
.favouriteColor
lives in ourFavouriteColorProvider
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.jsfunction 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
- ColorPicker rendered!
- ColorPicker rendered!