React Context Patterns
Structure context providers for performance and clean consumption
Overview
Context solves prop drilling, but naive usage causes every consumer to re-render whenever any part of the value changes. Good patterns split state from dispatch, memoize the provider value, and expose a custom hook that guards against use outside the provider.
Syntax / Usage
Split read and write concerns into separate contexts so components only subscribe to what they need.
import { createContext, useContext, useMemo, useReducer } from 'react'
const StateContext = createContext(null)
const DispatchContext = createContext(null)
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'reset':
return { count: 0 }
default:
return state
}
}
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
export function useCounterState() {
const ctx = useContext(StateContext)
if (ctx === null) throw new Error('useCounterState must be inside CounterProvider')
return ctx
}
export function useCounterDispatch() {
const ctx = useContext(DispatchContext)
if (ctx === null) throw new Error('useCounterDispatch must be inside CounterProvider')
return ctx
}
Examples
Memoize a combined value object so consumers don't re-render on unrelated parent updates:
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const value = useMemo(() => ({ theme, setTheme }), [theme])
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
Components that only dispatch never re-render when state changes:
function ResetButton() {
const dispatch = useCounterDispatch()
return <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
}
Common Mistakes
- Creating a new
valueobject each render withoutuseMemo, forcing all consumers to update - Storing rapidly changing values (like scroll position) in context
- Putting the entire app state in one context instead of scoping providers
- Skipping the null check, leading to cryptic errors when used outside a provider
- Nesting many providers manually instead of composing a single wrapper component
See Also
use-context react-custom-hooks react-performance-optimization