| name | state-management-patterns |
| description | Choose and implement state management — local state, Zustand, Redux Toolkit, Jotai, React Query/TanStack Query, URL state, and when to use each. Use when asked about "state management", "Zustand", "Redux Toolkit", "RTK", "React Query", "TanStack Query", "Jotai", "server state vs client state", "global state", "state colocation", "useQuery", "useMutation", "optimistic update", "cache invalidation", or "when to use Redux". Do NOT use for: React Server Components state — see nextjs-patterns. Do NOT use for: WebSocket state — see websocket-patterns.
|
| origin | yamtam-original |
| license | MIT © 2026 Vũ Văn Tâm |
| version | 1.0.0 |
| compatibility | React ≥ 18. Zustand v5, Redux Toolkit v2, TanStack Query v5, Jotai v2. |
When to Use
- Use when: prop drilling exceeds 2-3 levels
- Use when: async data fetching with loading/error/cache is tangled in useEffect
- Use when: multiple components read/write the same piece of state
- Do NOT use for: RSC data fetching — that's fetch() in server components (nextjs-patterns)
- Do NOT use for: form state — use react-hook-form (see frontend-patterns)
Decision Tree
State lives in...
└─ One component only → useState / useReducer
└─ URL (shareable, bookmarkable) → URL params / searchParams
└─ Server data (async, cached) → TanStack Query (React Query)
└─ Client UI state (cross-component, no async)
└─ Simple: 1-5 atoms → Jotai
└─ Complex: slices, middleware, devtools → Zustand
└─ Large team, strict patterns → Redux Toolkit
TanStack Query (Server State)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.users.get(userId),
staleTime: 60_000,
gcTime: 300_000,
});
if (isLoading) return <Skeleton />;
if (error) return <Error />;
return <Profile user={data} />;
}
function FollowButton({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: () => api..(id),
: (id) => {
queryClient.({ : [, id] });
prev = queryClient.([, id]);
queryClient.([, id], ({ ...old, : }));
{ prev };
},
: {
queryClient.([, id], ctx?.);
},
: {
queryClient.({ : [, id] });
},
});
;
}
Zustand (Client UI State)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartItem { id: string; qty: number; price: number; }
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
clear: () => void;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set(s => ({
items: s.items.find(i => i.id === item.)
? s..( i. === item. ? { ...i, : i. + item. } : i)
: [...s., item],
})),
: ( ({ : s..( i. !== id) })),
: ()..( sum + i. * i., ),
: ({ : [] }),
}),
{ : }
)
);
{ items, addItem, total } = ();
Jotai (Atomic State)
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
const themeAtom = atom<'light' | 'dark'>('light');
const sidebarAtom = atom(false);
const isDarkAtom = atom(get => get(themeAtom) === 'dark');
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
function Layout({ children }) {
const isDark = useAtomValue(isDarkAtom);
return <div className={isDark ? 'dark' : ''}>{children}</div>;
}
URL State (Shareable State)
import { useQueryState, parseAsInteger } from 'nuqs';
function ProductList() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [sort, setSort] = useQueryState('sort', { defaultValue: 'name' });
const [query, setQuery] = useQueryState('q', { defaultValue: '' });
}
Anti-Fake-Pass Rules
Before claiming state management is implemented, you MUST show:
Reference: gates/anti-fake-pass-gate.md