The most common mistake with React state management isn't reaching for Zustand too early — it's reaching for Context incorrectly, then blaming Context for problems that come from misusing it.
The Re-Render Problem
Every component that calls useContext(SomeContext) re-renders whenever the context value changes — even if the part that changed is irrelevant to it.
interface AppState {
user: User | null
sidebarOpen: boolean
notifications: Notification[]
}
const AppContext = createContext<AppState>(/* ... */)
function Navbar() {
const { user } = useContext(AppContext) // subscribes to EVERYTHING
return <nav>{user?.name}</nav>
}
When notifications updates every 30 seconds, Navbar re-renders even though it only cares about user. This is the source of most Context performance complaints.
The Actual Fix: Split the Context
One large context object is the problem. Split by update frequency:
const UserContext = createContext<User | null>(null)
const UIContext = createContext<{ sidebarOpen: boolean; toggleSidebar: () => void }>(/* ... */)
const NotificationContext = createContext<Notification[]>([])
function Navbar() {
const user = useContext(UserContext) // only re-renders when user changes
return <nav>{user?.name}</nav>
}
Most Context performance problems are solved by this split alone — no library needed.
When Context Is the Right Choice
Context excels for values that are stable and needed widely:
// Theme — changes once per session at most
const ThemeContext = createContext<ThemeContextValue | null>(null)
// Auth user — changes on sign in / sign out
const AuthContext = createContext<AuthContextValue | null>(null)
// Locale — changes when user switches language
const LocaleContext = createContext<LocaleContextValue | null>(null)
The common thread: stable values that don't update frequently.
Context also works well for SSR — Zustand is client-side only, but Context is readable in Server Components if you avoid state that requires useState.
When Zustand Fixes the Actual Problem
Zustand's core advantage: components subscribe to specific slices, not the whole store.
import { create } from 'zustand'
const useAppStore = create<AppStore>((set) => ({
user: null,
sidebarOpen: false,
notifications: [],
setUser: (user) => set({ user }),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
addNotification: (n) => set((s) => ({ notifications: [...s.notifications, n] })),
}))
function Navbar() {
// Only re-renders when user changes — ignores sidebarOpen and notifications
const user = useAppStore((s) => s.user)
return <nav>{user?.name}</nav>
}
function Sidebar() {
// Only re-renders when sidebarOpen changes
const { sidebarOpen, toggleSidebar } = useAppStore((s) => ({
sidebarOpen: s.sidebarOpen,
toggleSidebar: s.toggleSidebar,
}))
return <aside className={sidebarOpen ? 'w-64' : 'w-0'}>...</aside>
}
function NotificationBell() {
// Notifications updating 30x/day doesn't touch Navbar or Sidebar
const count = useAppStore((s) => s.notifications.length)
return <span>{count}</span>
}
Selector-based subscription is what Context fundamentally can't do without extra infrastructure.
Combining Both (Real Production Setup)
They're not mutually exclusive:
// Context: auth and theme — stable, available server-side
const AuthContext = createContext<AuthContextValue | null>(null)
const ThemeContext = createContext<ThemeContextValue | null>(null)
// Zustand: client-side app state — complex, selective subscriptions
const useUIStore = create<UIStore>(/* ... */)
const useDataStore = create<DataStore>(/* ... */)
Use Context for values that are stable and available at render time. Use Zustand for values that change frequently and need selective subscriptions.
The Decision Framework
New state?
│
├─ Stable? (theme, auth, locale) → React Context
├─ SSR/RSC readable? → React Context
├─ Simple prop drilling? → React Context (split contexts)
│
├─ Changes frequently? → Zustand
├─ Components need different slices? → Zustand
├─ Complex logic (derived, middleware)? → Zustand
│
└─ Fetched from an API? → TanStack Query (not Context or Zustand)
That last point matters: server state (API data) is a different category. Neither Context nor Zustand handles caching, background refetching, or stale-while-revalidate correctly. TanStack Query does.
Common Mistakes
Putting everything in one context object — the split is the fix.
Using Zustand for stable values — overkill, and adds client-only constraints.
Using Context for frequently-changing values — causes unnecessary re-renders across the tree.
Using Zustand for server state — that's TanStack Query's job.
Full article at stacknotice.com/blog/react-context-vs-zustand-2026
Top comments (0)