state-management
Frontend state management patterns: TanStack Query for server state, Zustand for client state, URL state, form state with React Hook Form, and when to use what. Prevents over-engineering and the most common state bugs.
Best use case
state-management is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Frontend state management patterns: TanStack Query for server state, Zustand for client state, URL state, form state with React Hook Form, and when to use what. Prevents over-engineering and the most common state bugs.
Teams using state-management should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/state-management/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How state-management Compares
| Feature / Agent | state-management | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Frontend state management patterns: TanStack Query for server state, Zustand for client state, URL state, form state with React Hook Form, and when to use what. Prevents over-engineering and the most common state bugs.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# State Management Skill
## When to Activate
- Deciding where to put state in a React/Vue/Svelte app
- Server data is going stale or showing inconsistent values
- Global state causing unnecessary re-renders
- Form state is getting complex
- URL not reflecting application state (back button broken)
- Migrating server data out of Zustand into TanStack Query to eliminate manual cache sync
- Adding optimistic updates to a mutation so the UI responds instantly before the server confirms
---
## State Type Decision Tree
```
Is this data from the server?
YES → TanStack Query (not a state library)
NO → Is it needed in multiple components far apart in the tree?
YES → Zustand (global client state)
NO → Is it navigation/filter/search state that should survive a refresh?
YES → URL state (search params)
NO → Is it form input?
YES → React Hook Form
NO → useState / useReducer (local)
```
**The most common mistake:** putting server data in Zustand. Use TanStack Query instead.
---
## Server State: TanStack Query
```typescript
// queries/users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query keys: structured, predictable, invalidation-friendly
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
// Fetch a single user
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => api.get<User>(`/users/${id}`),
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Keep in cache 10 minutes after unused
});
}
// Fetch list with filters
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => api.get<User[]>('/users', { params: filters }),
placeholderData: keepPreviousData, // Don't flash empty on filter change
});
}
// Mutation with optimistic update
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { id: string; update: Partial<User> }) =>
api.patch<User>(`/users/${data.id}`, data.update),
onMutate: async ({ id, update }) => {
// Cancel in-flight queries for this user
await queryClient.cancelQueries({ queryKey: userKeys.detail(id) });
// Snapshot previous value for rollback
const previous = queryClient.getQueryData<User>(userKeys.detail(id));
// Optimistically update
queryClient.setQueryData<User>(userKeys.detail(id), (old) =>
old ? { ...old, ...update } : old
);
return { previous };
},
onError: (_, { id }, context) => {
// Rollback on error
if (context?.previous) {
queryClient.setQueryData(userKeys.detail(id), context.previous);
}
},
onSettled: (_, __, { id }) => {
// Always refetch after mutation (source of truth from server)
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// Usage in component
function UserProfile({ id }: { id: string }) {
const { data: user, isLoading, error } = useUser(id);
const { mutate: updateUser, isPending } = useUpdateUser();
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return (
<form onSubmit={() => updateUser({ id, update: { name: 'New Name' } })}>
{/* ... */}
</form>
);
}
```
---
## Global Client State: Zustand
Only for state that is truly client-side and needed across the app (UI state, user preferences, shopping cart before checkout).
```typescript
// stores/ui.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface UIStore {
sidebarOpen: boolean;
theme: 'light' | 'dark' | 'system';
toggleSidebar: () => void;
setTheme: (theme: UIStore['theme']) => void;
}
export const useUIStore = create<UIStore>()(
devtools(
persist(
(set) => ({
sidebarOpen: true,
theme: 'system',
toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}),
{ name: 'ui-store' } // persisted to localStorage
)
)
);
// Cart store (not persisted to server yet)
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clear: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()((set, get) => ({
items: [],
addItem: (item) =>
set(state => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, item] };
}),
removeItem: (id) =>
set(state => ({ items: state.items.filter(i => i.id !== id) })),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
```
---
## URL State (nuqs / useSearchParams)
Use for: filters, pagination, search queries, active tabs — anything that should be bookmarkable.
```typescript
import { useQueryState, parseAsInteger, parseAsString } from 'nuqs';
function UserList() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''));
const [status, setStatus] = useQueryState('status');
const { data } = useUsers({ page, search, status });
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={status ?? ''} onChange={e => setStatus(e.target.value || null)}>
<option value="">All</option>
<option value="active">Active</option>
</select>
{data?.map(user => <UserRow key={user.id} user={user} />)}
<Pagination current={page} onChange={setPage} />
</>
);
}
// URL: /users?page=2&q=alice&status=active
// Back button works. Sharing link works. Refresh works.
```
---
## Form State: React Hook Form
```typescript
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, 'At least 2 characters'),
email: z.string().email('Invalid email'),
role: z.enum(['admin', 'member']),
});
type FormData = z.infer<typeof schema>;
function UserForm({ onSuccess }: { onSuccess: () => void }) {
const { mutate: createUser, isPending } = useCreateUser();
const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { role: 'member' },
});
const onSubmit = (data: FormData) => {
createUser(data, {
onSuccess,
onError: (err) => {
// Map server errors back to fields
if (err.field === 'email') {
setError('email', { message: err.message });
}
},
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<button disabled={isPending}>Save</button>
</form>
);
}
```
---
## Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
| Server data in Zustand | Manual sync, stale data, double fetch | TanStack Query |
| `useEffect` to sync state | Infinite loops, race conditions | Derive from source of truth |
| Huge single global store | All components re-render on any change | Multiple small stores |
| Filters/search in local state | Back button breaks, can't share URL | URL state |
| Form state in useState | Manual validation, reset logic | React Hook Form |
| Prop drilling 5+ levels | Brittle, painful refactoring | Zustand or Context |
---
## Checklist
- [ ] Server data in TanStack Query (not Zustand)
- [ ] Query keys are structured and hierarchical (for precise invalidation)
- [ ] Mutations invalidate the right query keys in `onSettled`
- [ ] Optimistic updates implemented for snappy UX on common mutations
- [ ] URL reflects all filterable/paginated state
- [ ] Forms use React Hook Form + Zod schema validation
- [ ] Zustand stores are small and single-purpose (not one mega-store)
- [ ] DevTools enabled in development (TanStack Query DevTools, Zustand DevTools)Related Skills
release-management
Release management: semantic versioning, conventional commits → CHANGELOG, git tagging, GitHub Releases, pre-release testing, and rollback procedures. Use /release to automate the process.
cost-management
Claude API cost awareness — token estimation, cost drivers, and efficiency strategies for Claude Code sessions
context-management
Context window auto-management — signals, strategies, and recovery protocol. Detects approaching context limits and guides compact vs checkpoint decisions to prevent lost work.
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
wireframing
Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
web-performance
Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.
wasm-performance
WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).
visual-testing
Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.
visual-identity
Brand identity development: color palette construction (primary/secondary/semantic/neutral), logo concept brief writing, typeface pairings, brand voice definition, mood board direction, and Brand Guidelines document structure. Use when establishing or evolving a visual brand — not for implementing existing tokens.