Technology · React
React Context Performance
Optimize context usage to avoid unnecessary re-renders with splitting and memoization.
TL;DR
- 01Split contexts into separate pieces to avoid cascading re-renders.
- 02Memoize context values to prevent unnecessary updates.
- 03Use useCallback for stable function references in context.
Context Re-render Problem
- All consumers re-render when context value changes, even partially.
const AppContext = createContext(); function Provider({ children }) { const [user, setUser] = useState(null); const [theme, setTheme] = useState("light"); return ( <AppContext.Provider value={{ user, theme, setUser, setTheme }}> {children} </AppContext.Provider> ); } // All components re-render if EITHER user or theme changes - This causes unnecessary re-renders of unrelated consumers.
- Splitting contexts solves this problem.
- Object literals as context values create a new reference on every render.
// Bad: new object created every render, all consumers re-render <MyContext.Provider value={{ user, setUser }}> {children} </MyContext.Provider> - Profile first with React DevTools before optimizing.
// React DevTools Profiler shows which components re-render // and how long each render takes // Only optimize when you see actual performance issues
Splitting Contexts
- Create separate contexts for independent pieces of state.
const UserContext = createContext(); const ThemeContext = createContext(); function Provider({ children }) { const [user, setUser] = useState(null); const [theme, setTheme] = useState("light"); return ( <UserContext.Provider value={{ user, setUser }}> <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> </UserContext.Provider> ); } - Components using only UserContext don't re-render on theme changes.
- Each context can be updated independently.
- Consumers subscribe only to the contexts they need.
- Split further by separating read and write contexts.
const UserStateContext = createContext(); // just the user value const UserDispatchContext = createContext(); // just setUser // Components that only read user never re-render when setUser changes function UserDisplay() { const user = useContext(UserStateContext); return <p>{user?.name}</p>; }
Memoizing Context Values
- Memoize context values to prevent unnecessary updates.
function Provider({ children }) { const [user, setUser] = useState(null); const userValue = useMemo(() => ({ user, setUser }), [user]); return ( <UserContext.Provider value={userValue}> {children} </UserContext.Provider> ); } - useMemo prevents new object creation on every render.
- Only updates when dependencies actually change.
- Essential for avoiding re-renders of memoized consumers.
- Include all values used from the context object in the dependency array.
const value = useMemo(() => ({ user, settings, updateUser }), [user, settings, updateUser]); // list every dependency
Memoized Consumers
- Wrap consumers with React.memo to skip re-renders.
const UserDisplay = React.memo(() => { const { user } = useContext(UserContext); return <div>{user?.name}</div>; }); - Memoization skips re-renders if props and context value don't change.
- Combine with memoized context value for best results.
- Still re-renders when context value actually changes.
- Pass a custom comparison function to React.memo for fine-grained control.
const UserDisplay = React.memo( ({ label }) => { const { user } = useContext(UserContext); return <p>{label}: {user?.name}</p>; }, (prevProps, nextProps) => prevProps.label === nextProps.label );
Callback Optimization
- Use useCallback to provide stable function references.
function Provider({ children }) { const [user, setUser] = useState(null); const updateUser = useCallback((newUser) => { setUser(newUser); }, []); const value = useMemo(() => ({ user, updateUser }), [user, updateUser]); return ( <UserContext.Provider value={value}> {children} </UserContext.Provider> ); } - Functions created every render cause unnecessary re-renders.
- useCallback provides a stable reference across renders.
- Combine with useMemo for complete optimization.
- Use useReducer dispatch instead of multiple callbacks for cleaner context.
// dispatch is always stable — no need for useCallback const [state, dispatch] = useReducer(reducer, initialState); const value = useMemo(() => ({ state, dispatch }), [state]);
Tip: Split contexts by domain (User, Theme, UI state) to avoid cascading re-renders when unrelated state changes.
Warning: Over-optimization with memoization adds complexity — only optimize contexts that are accessed by many components and update frequently.