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.
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>; }.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 |