//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Decide when to use React Context and when to use Zustand. Context works for low-frequency, theme-like state. Zustand excels for high-frequency, multi-consumer state that needs selective re-rendering.
// Context: Good for rarely-changing, provider-scoped state
const ThemeContext = createContext<"light" | "dark">("light");
// Zustand: Good for frequently-changing, global state with many consumers
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
}));// BEFORE: Context-based state (causes re-renders in all consumers)
// context/app-context.tsx
"use client";
import { createContext, useContext, useState, useCallback, ReactNode } from "react";
interface AppState {
user: { name: string } | null;
theme: "light" | "dark";
notifications: string[];
sidebarOpen: boolean;
}
interface AppContextValue extends AppState {
setUser: (user: AppState["user"]) => void;
toggleTheme: () => void;
addNotification: (msg: string) => void;
toggleSidebar: () => void;
}
const AppContext = createContext<AppContextValue | null>(null);
export function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AppState["user"]>(null);
const [theme, setTheme] = useState<AppState["theme"]>("light");
const [notifications, setNotifications] = useState<string[]>([]);
const [sidebarOpen, setSidebarOpen] = useState(true);
const toggleTheme = useCallback(() => {
setTheme((t) => (t === "light" ? "dark" : "light"));
}, []);
const addNotification = useCallback((msg: string) => {
setNotifications((n) => [...n, msg]);
}, []);
const toggleSidebar = useCallback(() => {
setSidebarOpen((s) => !s);
}, []);
// Problem: new object on every render -> all consumers re-render
return (
<AppContext.Provider
value={{
user, theme, notifications, sidebarOpen,
setUser, toggleTheme, addNotification, toggleSidebar,
}}
>
{children}
</AppContext.Provider>
);
}
export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be inside AppProvider");
return ctx;
};// AFTER: Zustand store (selective re-renders, no provider needed)
// stores/app-store.ts
import { create } from "zustand";
interface AppStore {
user: { name: string } | null;
theme: "light" | "dark";
notifications: string[];
sidebarOpen: boolean;
setUser: (user: AppStore["user"]) => void;
toggleTheme: () => void;
addNotification: (msg: string) => void;
toggleSidebar: () => void;
}
export const useAppStore = create<AppStore>((set) => ({
user: null,
theme: "light",
notifications: [],
sidebarOpen: true,
setUser: (user) => set({ user }),
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
addNotification: (msg) => set((s) => ({ notifications: [...s.notifications, msg] })),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));// Component migration
// BEFORE:
function Header() {
const { user, theme, toggleTheme } = useApp();
// Re-renders when notifications or sidebarOpen change too
return <header>...</header>;
}
// AFTER:
function Header() {
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);
const toggleTheme = useAppStore((s) => s.toggleTheme);
// Only re-renders when user or theme actually changes
return <header>...</header>;
}theme, it re-renders when notifications changes.createStore + useStore with a Context.When Context is still the right choice:
// Theme, locale, or auth status that rarely changes
// and naturally scopes to a subtree
const ThemeContext = createContext<"light" | "dark">("light");
// Feature flags per route segment
const FeatureFlagContext = createContext<Record<string, boolean>>({});
// Form state scoped to a single form
const FormContext = createContext<FormState | null>(null);Zustand with Context for SSR scoping:
import { createStore, useStore } from "zustand";
import { createContext, useContext, useRef } from "react";
type CounterStore = ReturnType<typeof createCounterStore>;
const createCounterStore = (initialCount = 0) =>
createStore<{ count: number; increment: () => void }>((set) => ({
count: initialCount,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
const CounterContext = createContext<CounterStore | null>(null);
export function CounterProvider({
children,
initialCount,
}: {
children: React.ReactNode;
initialCount?: number;
}) {
const storeRef = useRef<CounterStore>();
if (!storeRef.current) {
storeRef.current = createCounterStore(initialCount);
}
return (
<CounterContext.Provider value={storeRef.current}>
{children}
</CounterContext.Provider>
);
}
export function useCounterStore<T>(selector: (s: { count: number; increment: () => void }) => T) {
const store = useContext(CounterContext);
if (!store) throw new Error("Missing CounterProvider");
return useStore(store, selector);
}useContext calls to useStore(selector) calls. Do it incrementally.useMemo on the Context value helps but does not solve the fundamental re-render problem when any value changes.| Approach | Pros | Cons |
|---|---|---|
| React Context | Built-in, no dependencies, tree-scoped | Re-renders all consumers on any change |
| Zustand | Selective re-renders, no provider, simple API | Global singleton, SSR considerations |
| Jotai | Atomic model, provider-optional | Different mental model |
| Redux Toolkit | Mature, great DevTools | Boilerplate, steep learning curve |
| use(Context) + useMemo | Reduces some re-renders | Does not solve the core problem |
useContext on it.createStore to scope stores per request.// Before (Context):
const { user, theme } = useApp();
// After (Zustand):
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);useContext calls to useStore(selector).useMemo reduces re-renders caused by the provider re-rendering.const StoreContext = createContext<MyStore | null>(null);
function Provider({ children }) {
const storeRef = useRef(createStore<State>((set) => ({ ... })));
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
}createStore (vanilla) + Context + useStore for per-subtree instances.null check in useContext.devtools middleware to connect to Redux DevTools instead.Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥