Technology · React

React Context Performance

Optimize context usage to avoid unnecessary re-renders with splitting and memoization.

TL;DR
  1. 01Split contexts into separate pieces to avoid cascading re-renders.
  2. 02Memoize context values to prevent unnecessary updates.
  3. 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.

React Component PatternsReact Custom Hooks