React Context vs Zustand vs Jotai: Choosing Your State Manager
Global state management in React has more options than ever. Here's when to use each.
React Context: Built-In, Good for Low-Frequency Updates
Context re-renders every consumer on every update. This is fine for:
- Theme (changes rarely)
- Auth state (changes rarely)
- Feature flags (static after load)
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be within AuthProvider');
return ctx;
}
Don't use Context for: frequently updating state (cursor position, form inputs, real-time data) — the re-render cascade will destroy performance.
Zustand: Simple, Performant, No Boilerplate
Zustand is the sweet spot for most apps. Flat store, minimal setup, only re-renders components that subscribe to changed slices:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
const useCartStore = create<CartStore>()(devtools(persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}),
{ name: 'cart-storage' } // Persists to localStorage
)));
// Component only re-renders when items changes, not on unrelated state updates
function CartIcon() {
const count = useCartStore(state => state.items.length);
return <span>{count}</span>;
}
Jotai: Atomic State for Fine-Grained Reactivity
Jotai treats state as atoms — independent pieces that components subscribe to individually:
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// Primitive atoms
const countAtom = atom(0);
const userAtom = atom<User | null>(null);
// Derived atoms (computed, like useMemo but global)
const doubledCountAtom = atom((get) => get(countAtom) * 2);
// Async atoms
const userProfileAtom = atom(async (get) => {
const user = get(userAtom);
if (!user) return null;
return fetch(`/api/users/${user.id}`).then(r => r.json());
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Decision Guide
| Scenario | Use |
|---|---|
| Auth, theme, locale | Context |
| App-wide state, cart, UI state | Zustand |
| Highly dynamic, atom-level reactivity | Jotai |
| Server state (API data) | React Query |
Rule of thumb: React Query for server state, Zustand for client state, Context for config.
The full state management pattern — React Query + Zustand + Context where appropriate — is set up correctly in the AI SaaS Starter Kit so you start with the right architecture from day one.
Top comments (0)