HomeAbout Me

React Performance 2: Optimize Context

By Daniel Nguyen
Published in React JS
July 03, 2025
2 min read
React Performance 2: Optimize Context

Optimize Context

The way that context works is that whenever the provided value changes from one render to another, it triggers a re-render of all the consuming components (which will re-render whether or not they’re memoized).

So take this for example:

type CountContextValue = readonly [number, Dispatch<SetStateAction<number>>]
const CountContext = createContext<CountContextValue | null>(null)
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = [count, setCount]
return <CountContext value={value} {...props} />
}

Every time the <CountProvider /> is re-rendered, the value is brand new, so even though the count value itself may stay the same, all component consumers will be re-rendered.

This can be problematic in certain scenarios.

The quick and easy solution to this problem is to memoize the value that you provide to the context provider:

type CountContextValue = readonly [number, Dispatch<SetStateAction<number>>]
const CountContext = createContext<CountContextValue | null>(null)
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = useMemo(() => [count, setCount], [count])
return <CountContext value={value} {...props} />
}

By memoizing the value, you’re ensuring that the value is only re-created when the count value changes. As a result, the consuming components will only rerender when the count value changes.

Memoize Context

🧝‍♂️ I’ve taken the name and color state and combined them into a single FooterContext as well as extracted some of the UI into a FooterSetters component (for all the stuff that allows you to control the Footer). Feel free to

check my work
to inspect the changes if you want.

👨‍💼 Great, now if you pull up the React DevTools, you’ll notice all the components rerender whenever you change the counter in the App component. This is happening because the FooterContext is a new value prop every render.

So if you memoize the value prop with useMemo, you can prevent the unnecessary rerender of consuming components when the value prop doesn’t change.

Provider Component

👨‍💼 We’ve noticed that when we change some of the Footer context stuff, we’re actually rerendering the App and Main components even though those don’t have anything to do with the Footer context.

So we’re going to take advantage of React’s optimizations for reusing elements by creating a FooterProvider component and accepting children. That way whenever the Footer context changes, only the FooterProvider component and its consumers will rerender. React will be able to reuse the children element since that doesn’t change when the Footer’s state changes.

When you’re finished, changing the footer color of setting the footer name should not cause the App or Main components to rerender.

Split Context

👨‍💼 Something you may have noticed is that the FooterSetters component is rerendering whenever the footer state changes, but that component doesn’t actually depend on the footer state at all. All it cares about is the setter functions which never change!

Let’s assume that FooterSetters is an expensive component to render. How could we prevent it from rerendering unnecessarily when the footer state changes?

How about you split the context into two separate contexts: one for the state and one for the setters. For example:

function SomeProvider() {
const [state, setState] = useState()
const setters = useMemo(() => ({ setState }), [setState])
const stateValue = useMemo(() => ({ state }), [state])
return (
<StateContext value={stateValue}>
<SettersContext value={setters}>{children}</SettersContext>
</StateContext>
)
}

This way, FooterSetters can consume only the setters (which never change)!

Give that a shot in this exercise (and for extra credit you can also memo-ize the FooterSetters component and it will never rerender!).

This is going to require a fair bit of refactoring, but it should be pretty quick. Make sure you check out how components rerender in the React DevTools!

import { createContext, memo, use, useMemo, useState } from 'react'
import * as ReactDOM from 'react-dom/client'
const FooterStateContext = createContext<{
color: string
name: string
} | null>(null)
FooterStateContext.displayName = 'FooterStateContext'
const FooterDispatchContext = createContext<{
setColor: (color: string) => void
setName: (name: string) => void
} | null>({
setColor: () => {},
setName: () => {},
})
FooterDispatchContext.displayName = 'FooterDispatchContext'
function FooterProvider({ children }: { children: React.ReactNode }) {
const [color, setColor] = useState('black')
const [name, setName] = useState('')
const footerStateValue = useMemo(() => ({ color, name }), [color, name])
const footerDispatchValue = useMemo(() => ({ setColor, setName }), [])
return (
<FooterStateContext value={footerStateValue}>
<FooterDispatchContext value={footerDispatchValue}>
{children}
</FooterDispatchContext>
</FooterStateContext>
)
}
function useFooterState() {
const context = use(FooterStateContext)
if (!context) throw new Error('FooterStateContext not found')
return context
}
function useFooterDispatch() {
const context = use(FooterDispatchContext)
if (!context) throw new Error('FooterDispatchContext not found')
return context
}
const Footer = memo(function FooterImpl() {
const { color, name } = useFooterState()
return (
<footer style={{ color }}>
I am the ({color}) footer, {name || 'Unnamed'}
</footer>
)
})
function Main({ footer }: { footer: React.ReactNode }) {
const [count, setCount] = useState(0)
const increment = () => setCount((c) => c + 1)
return (
<div>
<button onClick={increment}>The count is {count}</button>
{footer}
</div>
)
}
const FooterSetters = memo(function FooterImplSetters() {
const { setColor, setName } = useFooterDispatch()
return (
<>
<div>
<p>Set the footer color:</p>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => setColor('black')}>Black</button>
<button onClick={() => setColor('blue')}>Blue</button>
<button onClick={() => setColor('green')}>Green</button>
</div>
</div>
<div>
<p>Set the footer name:</p>
<label>
Name:
<input onChange={(e) => setName(e.currentTarget.value)} />
</label>
</div>
</>
)
})
function App() {
const [appCount, setAppCount] = useState(0)
return (
<FooterProvider>
<div>
<FooterSetters />
<button onClick={() => setAppCount((c) => c + 1)}>
The app count is {appCount}
</button>
<Main footer={<Footer />} />
</div>
</FooterProvider>
)
}
const rootEl = document.createElement('div')
document.body.append(rootEl)
ReactDOM.createRoot(rootEl).render(<App />)

Tags

#React

Share

Previous Article
React Performance 1: Element Optimization

Table Of Contents

1
Optimize Context
2
Memoize Context
3
Provider Component
4
Split Context

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media