Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 189
Advanced

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.

Tips

  1. 01Use useReducer instead of useState in context providers — dispatch is always stable, so no useCallback needed.

Warnings

  1. 01Over-optimization with memoization adds complexity — only optimize contexts that are accessed by many components and update frequently.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 190
Advanced

React Context Performance

(continued)

Context Re-render Problem

  • Understand that every context consumer re-renders when any value in the context changes.

    const AppContext = createContext<{ user: User | null; theme: string } | undefined>(undefined);
    
    function AppProvider({ children }: { children: React.ReactNode }) {
      const [user, setUser] = useState<User | null>(null);
      const [theme, setTheme] = useState("light");
    
      // All consumers re-render if EITHER user or theme changes
      return (
        <AppContext.Provider value={{ user, theme, setUser, setTheme }}>
          {children}
        </AppContext.Provider>
      );
    }
    
  • Recognize that inline object literals create a new reference on every render, triggering all consumers.

    // Bad: new object on every parent render — all consumers re-render unnecessarily
    <MyContext.Provider value={{ user, setUser }}>
      {children}
    </MyContext.Provider>
    
    // Good: stable reference when user hasn't changed
    const value = useMemo(() => ({ user, setUser }), [user]);
    <MyContext.Provider value={value}>{children}</MyContext.Provider>
    
  • Profile with React DevTools before optimizing to confirm which components actually re-render.

    // Open React DevTools > Profiler tab
    // Record an interaction, then inspect the flame chart
    // Only optimize components that show unnecessary renders
    
  • Spot the problem by logging renders inside a context consumer.

    function ThemeButton() {
      const { theme } = useContext(AppContext)!;
      console.log("ThemeButton rendered"); // fires on every user change too
      return <button className={theme}>Click me</button>;
    }
    
  • Understand that splitting contexts into smaller pieces is the primary fix for this problem.

    // Instead of one big AppContext, use focused per-domain contexts:
    // UserContext → only user data
    // ThemeContext → only theme data
    // Each consumer subscribes only to what it needs
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 191
Advanced

React Context Performance

(continued)

Splitting Contexts

  • Create separate contexts for independent pieces of state so updates are isolated.

    const UserContext = createContext<{ user: User | null; setUser: (u: User | null) => void } | undefined>(undefined);
    const ThemeContext = createContext<{ theme: string; setTheme: (t: string) => void } | undefined>(undefined);
    
    function AppProvider({ children }: { children: React.ReactNode }) {
      const [user, setUser] = useState<User | null>(null);
      const [theme, setTheme] = useState("light");
      return (
        <UserContext.Provider value={{ user, setUser }}>
          <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
          </ThemeContext.Provider>
        </UserContext.Provider>
      );
    }
    
  • Confirm that user-only consumers no longer re-render on theme changes after the split.

    function UserBadge() {
      const { user } = useContext(UserContext)!;
      // Only re-renders when user changes — theme updates are ignored
      return <span>{user?.name}</span>;
    }
    
  • Let each context update independently so unrelated parts of the tree stay stable.

    function ThemeToggle() {
      const { theme, setTheme } = useContext(ThemeContext)!;
      // Changing theme does NOT re-render UserBadge above
      return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>{theme}</button>;
    }
    
  • Split read and write into separate contexts so read-only consumers never re-render from setter changes.

    const UserStateContext = createContext<User | null>(null);    // just the value
    const UserDispatchContext = createContext<(u: User | null) => void>(() => {}); // just the setter
    
    // Components that only display user data never re-render when setUser reference changes
    function UserDisplay() {
      const user = useContext(UserStateContext);
      return <p>{user?.name}</p>;
    }
    
  • Subscribe only to the contexts a component needs by consuming each context separately.

    function AdminPanel() {
      const { user } = useContext(UserContext)!;   // user updates trigger this
      // ThemeContext NOT consumed here — theme changes are irrelevant
      return user?.role === "admin" ? <div>Admin tools</div> : null;
    }
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 192
Advanced

React Context Performance

(continued)

Memoizing Context Values

  • Wrap the context value object in useMemo so a new reference is only created when dependencies change.

    function UserProvider({ children }: { children: React.ReactNode }) {
      const [user, setUser] = useState<User | null>(null);
    
      const userValue = useMemo(
        () => ({ user, setUser }),
        [user] // only creates a new object when user changes
      );
    
      return (
        <UserContext.Provider value={userValue}>
          {children}
        </UserContext.Provider>
      );
    }
    
  • List every value used from the context object in the dependency array to avoid stale closures.

    const value = useMemo(
      () => ({ user, settings, updateUser }),
      [user, settings, updateUser] // include all three dependencies
    );
    
  • Combine useMemo with stable function references so functions don't invalidate the memo.

    const updateUser = useCallback((newUser: User) => {
      setUser(newUser);
    }, []); // stable reference — empty deps because setUser is always stable
    
    const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
    
  • Verify that memoization is working by checking that consumers don't re-render on unrelated parent renders.

    // Parent re-renders due to its own state, but context value stays the same
    // Consumers should NOT re-render — confirm in React DevTools Profiler
    function ParentWithOwnState() {
      const [tick, setTick] = useState(0); // this state is unrelated to context
      return <button onClick={() => setTick(t => t + 1)}>Tick: {tick}</button>;
    }
    
  • Avoid memoizing the initial value inline — always use useMemo inside the provider component.

    // Wrong: initial value object is only created once but is a constant, not reactive
    <MyContext.Provider value={{ user: null, setUser: () => {} }}>
    
    // Right: useMemo inside the provider component reacts to state changes
    const value = useMemo(() => ({ user, setUser }), [user]);
    <MyContext.Provider value={value}>
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 193
Advanced

React Context Performance

(continued)

Memoized Consumers

  • Wrap context consumer components in React.memo so they skip re-renders when props are unchanged.

    const UserDisplay = React.memo(function UserDisplay() {
      const { user } = useContext(UserContext)!;
      // Skips re-renders when parent state changes but context value is stable
      return <div>{user?.name}</div>;
    });
    
  • Combine React.memo with a memoized context value for the best re-render reduction.

    // Provider memoizes the value object
    const value = useMemo(() => ({ user, setUser }), [user]);
    
    // Consumer is wrapped in React.memo
    const UserBadge = React.memo(() => {
      const { user } = useContext(UserContext)!;
      return <span>{user?.name}</span>;
    });
    // Only re-renders when the user object actually changes
    
  • Understand that React.memo still re-renders when the context value itself changes.

    // React.memo protects against prop changes, not context changes
    // If useContext returns a new reference, the component WILL re-render
    // Combine with useMemo on the provider value to control this
    
  • Pass a custom comparison function to React.memo for fine-grained prop-level control.

    const UserDisplay = React.memo(
      ({ label }: { label: string }) => {
        const { user } = useContext(UserContext)!;
        return <p>{label}: {user?.name}</p>;
      },
      (prevProps, nextProps) => prevProps.label === nextProps.label // only re-render if label changes
    );
    
  • Extract the context-consuming logic into a child component so React.memo can protect the expensive UI.

    // Expensive chart component is memoized; only re-renders when chartData changes
    const Chart = React.memo(({ chartData }: { chartData: number[] }) => {
      return <canvas>{/* expensive render */}</canvas>;
    });
    
    function ChartContainer() {
      const { chartData } = useContext(DashboardContext)!;
      return <Chart chartData={chartData} />; // passes stable reference when memoized
    }
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 194
Advanced

React Context Performance

(continued)

Callback Optimization

  • Wrap context callbacks in useCallback so their references stay stable across parent renders.

    function UserProvider({ children }: { children: React.ReactNode }) {
      const [user, setUser] = useState<User | null>(null);
    
      const updateUser = useCallback((newUser: User) => {
        setUser(newUser); // setUser is stable, so deps array is empty
      }, []);
    
      const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
    
      return (
        <UserContext.Provider value={value}>{children}</UserContext.Provider>
      );
    }
    
  • Understand that functions created inline on every render invalidate the memoized context object.

    // Bad: inline function creates new reference every render
    const value = useMemo(() => ({
      user,
      updateUser: (u: User) => setUser(u), // new reference each render
    }), [user]); // updateUser changes every render, memo is useless
    
    // Good: useCallback gives updateUser a stable reference
    const updateUser = useCallback((u: User) => setUser(u), []);
    const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
    
  • Use useReducer dispatch instead of multiple callbacks — dispatch is always stable, no useCallback needed.

    const [state, dispatch] = useReducer(userReducer, initialState);
    
    // dispatch never changes reference — safe to include in memoized context value
    const value = useMemo(() => ({ state, dispatch }), [state]);
    
  • Compose multiple stable callbacks into a single memoized actions object for cleaner context values.

    const actions = useMemo(() => ({
      login: (credentials: Credentials) => dispatch({ type: "LOGIN", payload: credentials }),
      logout: () => dispatch({ type: "LOGOUT" }),
      updateProfile: (data: Partial<User>) => dispatch({ type: "UPDATE", payload: data }),
    }), [dispatch]); // dispatch is stable, so actions object is stable
    
  • Prefer useReducer over useState in context providers to eliminate most useCallback needs.

    // useState approach: need useCallback for every mutator
    const [user, setUser] = useState(null);
    const updateName = useCallback((name) => setUser(u => ({ ...u!, name })), []);
    
    // useReducer approach: dispatch is always stable — no useCallback needed
    const [state, dispatch] = useReducer(reducer, { user: null });
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Context Performance
Chapter 27 · Page 195
Advanced

React Context Performance

(FAQ)

FAQ

Every component subscribed to a context re-renders whenever any value in that context changes, even if the component only uses an unrelated field. Split your context into separate providers per concern so updates to one domain don't trigger consumers of another.

Wrap the value object in useMemo with its dependencies: const value = useMemo(() => ({ user, setUser }), [user]). Without this, a new object reference is created on every parent render, causing all consumers to re-render even when the data hasn't changed.

Wrap a consumer in React.memo when it receives context values as props passed down from a parent rather than via useContext directly — this lets React bail out of re-rendering if the specific props it receives haven't changed. Components using useContext directly still re-render on context changes regardless of React.memo.

Functions defined inline in a component recreate on every render, making them new references that invalidate the memoized context object. Wrap each function with useCallback and declare its dependencies so the reference stays stable between renders.

There's no hard limit — providers are cheap and multiple providers composed in a tree is the recommended pattern. Combining unrelated state into one context trades boilerplate reduction for broader re-render scope, which is usually the wrong tradeoff for frequently-updating values.