DEV Community

Atlas Whoff
Atlas Whoff

Posted on

React Context vs Zustand vs Jotai: Choosing Your State Manager

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;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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)