State Management Patterns
State Categories
| Concern |
Tool |
| Server/remote state |
TanStack Query, SWR |
| Client UI state |
Zustand, Jotai |
| URL/shareable state |
search params, nuqs |
| Form state |
React Hook Form |
| Complex flows |
XState |
| Global derived state |
Jotai atoms |
TanStack Query (Server State)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch with caching
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 min
});
}
// Optimistic mutation
function useUpdateUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: (user) => fetch(`/api/users/${user.id}`, { method: 'PUT', body: JSON.stringify(user) }),
onMutate: async (updated) => {
await qc.cancelQueries({ queryKey: ['users'] });
const previous = qc.getQueryData(['users']);
qc.setQueryData(['users'], (old) => old.map(u => u.id === updated.id ? updated : u));
return { previous };
},
onError: (_, __, ctx) => qc.setQueryData(['users'], ctx.previous),
onSettled: () => qc.invalidateQueries({ queryKey: ['users'] }),
});
}
Zustand (Client State)
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clear: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
devtools(persist(immer((set, get) => ({
items: [],
addItem: (item) => set(state => {
const existing = state.items.find(i => i.id === item.id);
if (existing) existing.qty += 1;
else state.items.push({ ...item, qty: 1 });
}),
removeItem: (id) => set(state => {
state.items = state.items.filter(i => i.id !== id);
}),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
})), { name: 'cart' }))
);
Jotai (Atomic State)
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// Primitive atom
const countAtom = atom(0);
// Derived atom (computed)
const doubleAtom = atom(get => get(countAtom) * 2);
// Writable derived
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
// Async atom
const userAtom = atom(async () => {
const res = await fetch('/api/me');
return res.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
const double = useAtomValue(doubleAtom);
return <button => setCount(c => c + 1)}>{count} (×2: {double})</button>;
}
URL State with nuqs
import { useQueryState, parseAsInteger, parseAsString } from 'nuqs';
function ProductList() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''));
const [sort, setSort] = useQueryState('sort', parseAsString.withDefault('name'));
// State is in URL: /products?page=2&q=phone&sort=price
// Shareable, back-button safe, SSR-compatible
}
XState (Complex Flows)
import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';
const checkoutMachine = createMachine({
id: 'checkout',
initial: 'cart',
context: { items: [], error: null },
states: {
cart: { on: { CHECKOUT: 'shipping' } },
shipping: { on: { BACK: 'cart', NEXT: 'payment' } },
payment: {
on: { BACK: 'shipping', SUBMIT: 'processing' }
},
processing: {
invoke: { src: 'submitOrder',
onDone: { target: 'success' },
onError: { target: 'payment', actions: assign({ error: (_, e) => e.data.message }) }
}
},
success: { type: 'final' },
}
});
function Checkout() {
const [state, send] = useMachine(checkoutMachine, {
actors: { submitOrder: (ctx) => submitOrderAPI(ctx.items) }
});
return state.matches('cart') ? <Cart => send('CHECKOUT')} /> : null;
}