6 min read
0%

Context API Re-render Optimization

Back to Blog
Context API Re-render Optimization

Context API Re-render Optimization

Context is React’s built-in prop-passing alternative. Its re-render behavior is frequently misunderstood — every consumer re-renders whenever the context value changes, regardless of which part of the value they use.

The Re-render Problem

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  return (
    <UserContext.Provider value={{ user, theme, setUser, setTheme }}>
      {children}
    </UserContext.Provider>
  );
}

Every time theme changes, every component consuming UserContext re-renders — even if it only cares about user. Context uses reference equality on the value object, and a new object is created every render.

Fix 1: Split Contexts

Separate frequently-changing values into their own context:

const UserContext = createContext();
const ThemeContext = createContext();

function Providers({ 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 subscribe to only what they need.

Fix 2: Memoize the Context Value

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

The context value only changes when user changes. Note: setUser is already stable (state setters don’t change).

Fix 3: Separate State and Dispatch

Split into two contexts — one for state (changes), one for dispatch (stable):

const StateContext = createContext();
const DispatchContext = createContext();

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={state}>
        {children}
      </StateContext.Provider>
    </DispatchContext.Provider>
  );
}

dispatch is stable — components that only dispatch never re-render when state changes.

Fix 4: Memoize Derived Values

function useUserName() {
  const { user } = useContext(UserContext);
  return useMemo(() => user?.name, [user]);
}

Does not prevent the context consumer re-render, but memoizes an expensive derivation. Useful when the derived computation is heavy.

When Context Is the Wrong Tool

Context re-renders all consumers on every value change. For high-frequency updates or many consumers, consider:

  • Zustand — selector-based, rerenders only subscribers of changed slices
  • Jotai — atom-based, extremely granular subscriptions
  • Redux Toolkit — battle-tested, reselect for memoized selectors

Context excels for low-frequency updates: theme, locale, auth state, feature flags.


Canvas is not supported in your browser