Skip to content

React Context Performance Patterns β€” Avoiding Unnecessary Re-Renders

DodaTech 7 min read

In this tutorial, you'll learn about React Context Performance Patterns. We cover key concepts, practical examples, and best practices.

React Context re-renders every consumer when the context value changes, even if only part of the data was updated β€” understanding when and how to split, memoize, and bypass context is essential for performance.

What You'll Learn

Optimize React Context usage by splitting contexts, memoizing values, using selectors, and knowing when to replace context with Zustand or Jotai for high-frequency updates.

Why It Matters

A single context holding auth state, theme, and notification data causes all consumers to re-render whenever any value changes. In a dashboard with 50+ components consuming the same context, a notification badge update re-renders the entire tree. Proper context patterns can eliminate 80-90% of unnecessary re-renders.

Real-World Use

A DodaZIP file manager panel uses three separate contexts: ThemeContext (rarely changes), UserContext (changes on login/logout), and FileSelectionContext (changes on every click). Splitting them means clicking a file re-renders only the selection UI, not the theme or user panels.

The Re-Render Problem

Every consumer of a context re-renders when the context value changes, even if it only uses a different part of the value:

// BAD: Single context causes cascading re-renders
interface AppState {
  user: User | null;
  theme: "light" | "dark";
  notifications: Notification[];
}

const AppContext = createContext<AppState>(null!);

function AppProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<AppState>({
    user: null,
    theme: "light",
    notifications: [],
  });

  // Every update creates a new object β€” ALL consumers re-render
  return <AppContext.Provider value={state}>{children}</AppContext.Provider>;
}

Expected output: When notifications updates (every few seconds), every consumer β€” including the theme toggle and user avatar β€” re-renders. React DevTools shows all components flashing on each notification poll.

Solution 1: Split Contexts

Separate unrelated state into multiple contexts:

// GOOD: Split unrelated state into separate contexts
const ThemeContext = createContext<"light" | "dark">("light");
const UserContext = createContext<User | null>(null);
const NotificationContext = createContext<Notification[]>([]);
const SetNotificationContext = createContext<Dispatch<SetStateAction<Notification[]>>>(() => {});

function AppProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const [user, setUser] = useState<User | null>(null);
  const [notifications, setNotifications] = useState<Notification[]>([]);

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <NotificationContext.Provider value={notifications}>
          <SetNotificationContext.Provider value={setNotifications}>
            {children}
          </SetNotificationContext.Provider>
        </NotificationContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Only Notification consumers re-render when notifications change
// Theme toggle and user avatar stay mounted without re-render

Expected output: A notification update re-renders only components consuming NotificationContext or SetNotificationContext. The theme switcher and user avatar do not re-render. React DevTools confirms isolated updates.

Solution 2: Memoize Context Values

If a context value contains computed data, memoize to prevent unnecessary re-renders:

// GOOD: Memoize context values to preserve reference equality
interface AuthContextValue {
  user: User | null;
  permissions: string[];
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  hasPermission: (perm: string) => boolean;
}

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = useCallback(async (email: string, password: string) => {
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    const data = await res.json();
    setUser(data.user);
  }, []);

  const logout = useCallback(() => setUser(null), []);

  const value = useMemo<AuthContextValue>(() => ({
    user,
    permissions: user?.permissions ?? [],
    login,
    logout,
    hasPermission: (perm: string) => user?.permissions?.includes(perm) ?? false,
  }), [user, login, logout]);

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

// Without useMemo: every AuthProvider render creates a new object.
// With useMemo: object reference changes only when user changes.

Expected output: AuthContext.Provider receives the same object reference when user has not changed. Consumer components that memoize with React.memo or useMemo skip re-rendering. The login and logout callbacks are stable across renders.

Solution 3: Context Selectors with useSyncExternalStore

For granular subscriptions, use useSyncExternalStore to read only parts of a context:

// ADVANCED: Selector pattern using useSyncExternalStore
function createStore<State>(initialState: State) {
  let state = initialState;
  const listeners = new Set<() => void>();

  return {
    getState: () => state,
    setState: (partial: Partial<State>) => {
      state = { ...state, ...partial };
      listeners.forEach(l => l());
    },
    subscribe: (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

const store = createStore({
  user: null as User | null,
  theme: "light" as "light" | "dark",
  notifications: [] as Notification[],
});

function useStoreSelector<Selected>(selector: (state: typeof store.getState()) => Selected) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
  );
}

function NotificationBadge() {
  // Only re-renders when notifications.length changes, not when user or theme change
  const count = useStoreSelector(state => state.notifications.length);
  return count > 0 ? <span style={{ background: "#d32f2f", color: "#fff", borderRadius: "50%", padding: "2px 6px" }}>{count}</span> : null;
}

Expected output: NotificationBadge subscribes to the store and re-renders only when notifications.length changes. Updating user or theme does not trigger a re-render. This is the same mechanism Zustand and Jotai use internally.

Common Errors

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

You are calling a state setter during render instead of in an event handler or effect. In context providers, ensure setState calls happen inside useEffect, useCallback, or event handlers β€” never in the render body.

Error: Context value is undefined in consumer

The consumer component is rendered outside the provider tree. Wrap your app or the relevant subtree with <MyContext.Provider value={...}>. Check that the provider is at a higher level than all consumers.

Error: 'useContext' must be called inside a function component

You called useContext outside a React component or inside a regular JavaScript function. Context hooks can only be used in function components or custom hooks. Create a custom hook like useTheme() that wraps useContext.

Error: 'useSyncExternalStore' received a subscribe function that did not return an unsubscribe function

The subscribe function provided to useSyncExternalStore must return a cleanup function that removes the listener. Without it, listeners accumulate and cause memory leaks. Ensure your store's subscribe returns () => listeners.delete(listener).

Warning: Cannot update a component while rendering a different component

A context update triggered a state update in another component during render. This usually happens when a context value changes and causes a setter call in a consumer's render body. Move state updates to useEffect or wrap in startTransition

Practice Questions

  1. Why does a single React Context cause unnecessary re-renders? React Context uses referential equality (Object.is) to detect changes. When the context value is an object literal or new array, every provider render creates a new reference, causing all consumers to re-render even if the data did not logically change.

  2. How does splitting contexts improve performance? Each context has its own set of consumers. Updating one context (e.g., notifications) only re-renders its consumers. Other contexts (theme, user) remain unchanged, so their consumers do not re-render. This isolates render boundaries.

  3. When should you use Zustand or Jotai instead of Context? When you have high-frequency updates (every 100ms or faster), deeply nested state that only affects small subtrees, or when you need fine-grained selectors without provider nesting. Context is fine forδ½Žι’‘ or static data like theme, locale, or auth.

Challenge

Create a dashboard with 5 context-driven sections: header, sidebar, main content, notifications panel, and footer. Each section consumes a shared context. Profile re-renders when one value changes. Then refactor with split contexts and useSyncExternalStore selectors. Compare the re-render count in React DevTools Profiler.

Real-World Task

Examine your current React app's context structure. Identify the largest context (most consumers). Profile a re-render when one field changes. Split the context into logical groups based on which components actually consume which fields. Measure the reduction in re-renders and component render time.

Mini Project: File Manager with Optimized Context

Build a file manager UI with:

  • Three contexts: ThemeContext, NavigationContext (current folder), SelectionContext (selected files)
  • File grid that re-renders on navigation changes only
  • Selection sidebar that re-renders on selection changes only
  • Theme toggle that never re-renders other components
  • Profile and verify that clicking a file does not re-render the theme toggle or breadcrumbs

Learning Path

flowchart LR
  A[React Context API] --> B[React State Management]
  B --> C[React Context Performance Patterns]
  C --> D[React Compiler Explained]
  D --> E[React Concurrent Features]
  style C fill:#4a90d9,color:#fff

Next Steps

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. DodaZIP's file manager uses three split contexts to ensure selecting a file in the grid never re-renders the theme switcher or the user avatar β€” only the selection UI updates, keeping the interface smooth even with thousands of files.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro