Optimizing React Context and Re-renders

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

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.
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.
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.
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.
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.
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.
useMemo hookYou 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.
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 ProviderYou 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]);
If you want to learn more fancy stuff, you may want to read the docs about Scaling Up with Reducer and Context.