Multiple Contexts, Performance and When to Use Zustand

A well-architected MERN Blog uses multiple focused contexts rather than one monolithic global store. AuthContext owns authentication, ThemeContext owns the colour scheme, NotificationContext owns toast messages. Each context is independent — components subscribe only to what they need, and a change in one context does not cause unnecessary re-renders in components that only consume a different context. In this final lesson you will compose multiple contexts, measure and address the most common performance pitfall (context value object instability), and understand when the MERN Blog has grown large enough to benefit from a dedicated state management library like Zustand.

Composing Multiple Contexts

// src/main.jsx — stacked providers
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <ThemeProvider>
        <AuthProvider>
          <NotificationProvider>
            <App />
          </NotificationProvider>
        </AuthProvider>
      </ThemeProvider>
    </BrowserRouter>
  </StrictMode>
);

// The order matters: inner providers can consume outer contexts.
// AuthProvider can call useTheme() because it is inside ThemeProvider.
// NotificationProvider can call useAuth() because it is inside AuthProvider.
Note: Stacking many Providers in main.jsx can look unwieldy. A common pattern is to create a single AppProviders wrapper component that composes all providers internally. Import one component in main.jsx and add new providers in one place: function AppProviders({ children }) { return <ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>; }.
Tip: Context splitting — keeping each context focused on one concern — is the key performance optimisation. If AuthContext changes (user logs out), only components that consume AuthContext re-render. Components that only consume ThemeContext are completely unaffected. One large context with everything in it would cause every context consumer across the entire app to re-render on every change to any piece of state.
Warning: Even with memoised context values, every component that calls useContext(SomeContext) re-renders when that context’s value changes — even if the specific part of the value the component uses did not change. For example, if AuthContext exposes { user, loading } and loading changes, every consumer of AuthContext re-renders even if they only use user. Split loading into its own context or use React.memo on the consumers to address this.

The AppProviders Wrapper Pattern

// src/context/AppProviders.jsx
import { ThemeProvider }       from './ThemeContext';
import { AuthProvider }        from './AuthContext';
import { NotificationProvider } from './NotificationContext';

function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

export default AppProviders;

// src/main.jsx — clean, one import
import AppProviders from '@/context/AppProviders';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <AppProviders>
        <App />
      </AppProviders>
    </BrowserRouter>
  </StrictMode>
);

Context Performance — Identifying Re-render Issues

// React DevTools Profiler highlights which components re-render
// and why. Install the React Developer Tools browser extension.

// To diagnose context re-renders:
// 1. Open React DevTools → Profiler tab
// 2. Click Record, perform an action (e.g. toggle theme)
// 3. Stop recording — see which components re-rendered
// 4. A component that re-rendered "because of context" indicates
//    it is consuming a context whose value changed

// ── Optimisation 1: Split context by update frequency ─────────────────────────
// Instead of one UserContext:
const UserContext = createContext(); // { user, preferences, notifications }

// Split into two:
const UserAuthContext    = createContext(); // { user } — rarely changes
const UserPrefsContext   = createContext(); // { preferences } — changes per action
// Components that only need user subscribe only to UserAuthContext
// They don't re-render when preferences change

// ── Optimisation 2: Memoize consumers with React.memo ─────────────────────────
const Header = React.memo(function Header() {
  const { user } = useAuth();
  return <header>{user?.name}</header>;
});
// Header only re-renders if user changes — not if the whole app re-renders

// ── Optimisation 3: Use context selector pattern with useSyncExternalStore ─────
// (Advanced — typically only needed at the scale where Zustand is warranted)

When to Move to Zustand

Signal Action
Context re-renders are visibly causing performance issues Consider Zustand for that data
State needs to be updated from many unrelated parts of the app Zustand’s flat store is cleaner than deeply nested context
You need fine-grained subscriptions (only re-render when x changes) Zustand supports per-slice subscriptions natively
Complex derived state with expensive computation Zustand + Immer or Jotai’s derived atoms
Auth + theme + notifications in a solo or small team project Context is fine — do not over-engineer

A Minimal Zustand Store — For Comparison

// npm install zustand — for reference (not used in this series)
import { create } from 'zustand';

const useAuthStore = create((set) => ({
  user:    null,
  loading: true,

  // Actions — synchronous
  setUser:    (user)   => set({ user }),
  setLoading: (bool)   => set({ loading: bool }),
  logout:     ()       => { localStorage.removeItem('token'); set({ user: null }); },
}));

// Usage — subscribes only to the specific slice used
function Header() {
  // Only re-renders when user changes — NOT when loading changes
  const user   = useAuthStore(state => state.user);
  const logout = useAuthStore(state => state.logout);
  return <button onClick={logout}>{user?.name}</button>;
}

// Comparison: useContext(AuthContext) re-renders on any AuthContext change.
// useAuthStore(state => state.user) only re-renders when user changes.
// This fine-grained subscription is the main advantage of Zustand over Context.

Common Mistakes

Mistake 1 — Putting everything in one context

❌ Wrong — single massive context causes all consumers to re-render on any change:

const AppContext = createContext();
// value = { user, theme, notifications, posts, filters, pagination }
// Changing page number → Header re-renders (only needs user)
// Adding a notification → PostList re-renders (does not use notifications)

✅ Correct — one focused context per concern, consumed only where needed.

Mistake 2 — Not memoising the context value

❌ Wrong — inline object in Provider’s value prop:

return <AuthContext.Provider value={{ user, loading, login, logout }}>
// New object created every time AuthProvider renders → all consumers re-render

✅ Correct — memoised value:

const value = useMemo(() => ({ user, loading, login, logout }), [user, loading, login, logout]);
return <AuthContext.Provider value={value}> // ✓ stable until deps change

Mistake 3 — Prematurely adding Zustand for a small app

❌ Wrong — adding Zustand, Immer, and persist middleware for a 5-page blog app.

✅ Correct — Context is perfectly adequate for the MERN Blog’s auth, theme, and notification state. Add Zustand when context performance issues are measurable and confirmed with the Profiler, not speculatively.

Quick Reference

Pattern Code
Compose providers Wrap in AppProviders component
Split for performance Separate contexts per concern
Memoize value useMemo(() => ({ ... }), [deps])
Memoize functions useCallback(fn, deps)
Memo consumers React.memo(Component)
Profile re-renders React DevTools → Profiler tab
Upgrade when Profiler shows measurable re-render performance issues

🧠 Test Yourself

Your AuthContext holds { user, loading, notifications } in one context value. The notification list updates every few seconds. You notice your Header and PostCard components re-render constantly. What is the most targeted fix?