React Context and Re-renders
In this post, we'll dive into how re-renders happen in the situation of React Context.
I want us to understand how it works and also how you can optimize if needed.
Recap of using Context
Let's quickly look at some code to remind ourselves how Context is used:
// DashboardContext.js
import { createContext, useContext } from 'react'
type DashboardContextType = {
name: string
age: number
}
const DashboardContext =
createContext<DashboardContextType | null>(null)
type DashboardProviderType = {
children: React.ReactNode
name: string
age: number
}
export const DashboardProvider = ({
children,
name,
age
}: DashboardProviderType) => {
return (
<DashboardContext.Provider value={{ name, age }}>
{children}
</DashboardContext.Provider>
)
}
export const useDashboardContext = () => {
const context = useContext(DashboardContext)
if (!context) {
throw new Error('useDashboardContext must be used within an DashboardProvider')
}
return context
}
How to use the provider:
// App.js
<DashboardProvider name="John" age={20}>
<Dashboard />
</DashboardProvider>
The user values name
and age
are passed to the provider. They would in a real scenario e.g. be fetched from an API.
In Dashboard
, we can use the useDashboardContext
hook to access the values:
// Dashboard.js
const { name, age } = useDashboardContext();
You can also have other components use the useDashboardContext
hook to access the values. They would have to be inside the DashboardProvider
component. So next to Dashboard
component itself or inside of it.
When will a component re-render?
Any component that consumes the context via useContext
will re-render when the context changes.
If the component is inside the DashboardProvider
but doesn't consume the context, it won't re-render when the context changes.
Oops, we have an issue
Many components are consuming the context and re-render whenever the context changes.
We want to minimize the re-renders as it's getting expensive.
Let's go over things you can do in this situation.
Evaluate what's causing the re-renders
Start by understanding the value of your context.
Pin down why re-renders are happening and when they're happening.
I recommend using the React DevTools to do this.
Split the context
If you've have a single context with a dozen of values, you can look at which components need what specific value.
Some components are related and are supposed to re-render together while others are completely unrelated.
Split the context into multiple contexts if they have values that are unrelated.
A sign here is to see which components are using the same values from the context.
Use the useMemo
hook
You can use the useMemo
hook to memoize the values of the context.
This can help when you've one Provider, but different states. Mind you, you'll have to create multiple consumption hooks if you take this approach.
For example:
import React, { createContext, useContext, useState, useMemo } from "react";
const DashboardContext = createContext();
export function DashboardProvider({ children }) {
const [userInfo, setUserInfo] = useState({
name: "Alice",
email: "alice@example.com",
});
const [systemMetrics, setSystemMetrics] = useState({ cpu: 50, network: 20 });
const userInfoValue = useMemo(
() => ({
...userInfo,
setUserInfo,
}),
[userInfo]
);
const systemMetricsValue = useMemo(
() => ({
...systemMetrics,
setSystemMetrics,
}),
[systemMetrics]
);
return (
<DashboardContext.Provider value={{ userInfoValue, systemMetricsValue }}>
{children}
</DashboardContext.Provider>
);
}
export function useUserInfo() {
const context = useContext(DashboardContext);
return context.userInfoValue;
}
export function useSystemMetrics() {
const context = useContext(DashboardContext);
return context.systemMetricsValue;
}
Every component using useUserInfo
would only re-render when the userInfo
changes. Same goes for useSystemMetrics
.
This is also called "selective rendering". Specific hooks to only return what the component needs. If other data changes outside of what the specific hooks return, the component doesn't re-render.
State from above
If you've the issue where state comes from above the component that uses the Provider, you can use the React.memo
function to memoize the component itself so that it doesn't re-render when its props changes.
This from my experience is rarely an issue, but worth mentioning:
const ConsumerComponent = React.memo(function ({ value }) {
// Component implementation
});
useMemo
on the value passed to Provider
You may be creating a new object every time the component re-renders, which causes the Provider to believe the value has changed, and then it re-renders all of its components.
Be careful, this also happens if you inline the Provider value.
// Bad
value={{ name, age }}
// Good
const value = useMemo(() => ({ name, age }), [name, age]);
Further read
If you want to learn more fancy stuff, you may want to read the docs about Scaling Up with Reducer and Context.