State Management
Modern state management patterns for React applications.
Decision Tree
User request → What type of state?
│
├─ Client State (UI, local)
│ ├─ Zustand → Simple, minimal boilerplate
│ ├─ Jotai → Atomic, fine-grained
│ ├─ Valtio → Proxy-based, mutable API
│ └─ Context → Built-in, simple cases
│
├─ Server State (API data)
│ ├─ TanStack Query → Caching, background sync
│ ├─ SWR → Lightweight, stale-while-revalidate
│ └─ RTK Query → Redux ecosystem
│
├─ Form State
│ ├─ React Hook Form → Performance, validation
│ ├─ Formik → Mature, feature-rich
│ └─ Conform → Progressive enhancement
│
└─ URL State
├─ nuqs → Type-safe URL params
└─ useSearchParams → Built-in Next.js
Quick Start
Zustand Setup
pnpm add zustand
// stores/useUserStore.ts
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
interface User {
id: string;
email: string;
name: string;
}
interface UserState {
user: User | null;
isAuthenticated: boolean;
setUser: (user: User) => void;
logout: () => void;
}
export const useUserStore = create<UserState>()(
devtools(
persist(
(set) => ({
user: null,
isAuthenticated: false,
setUser: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}),
{ name: "user-storage" }
)
)
);
TanStack Query Setup
pnpm add @tanstack/react-query @tanstack/react-query-devtools
// lib/query-client.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
refetchOnWindowFocus: false,
},
},
});
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
export function useUsers() {
return useQuery({
queryKey: ["users"],
queryFn: () => api.get("/users"),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: ["users", id],
queryFn: () => api.get(`/users/${id}`),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserInput) => api.post("/users", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}
React Hook Form + Zod
pnpm add react-hook-form @hookform/resolvers zod
// components/UserForm.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const userSchema = z.object({
email: z.string().email("Invalid email"),
name: z.string().min(2, "Name too short"),
password: z.string().min(8, "Password must be 8+ characters"),
});
type UserFormData = z.infer<typeof userSchema>;
export function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register("name")} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}
<input {...register("password")} type="password" placeholder="Password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</form>
);
}
State Type Comparison
| State Type |
Solution |
Persistence |
Sync |
| UI State |
Zustand |
Optional |
No |
| Server State |
TanStack Query |
Cache |
Background |
| Form State |
React Hook Form |
No |
No |
| URL State |
nuqs |
URL |
Browser |
| Auth State |
Zustand + persist |
LocalStorage |
No |
Reference Files
Best Practices
- Separate concerns: UI state vs server state vs form state
- Colocate state: Keep state close to where it's used
- Derive don't duplicate: Compute values from source of truth
- Normalize server data: Avoid nested structures
- Optimistic updates: Instant feedback, rollback on error
- Persist wisely: Only what's necessary
- Type everything: Full TypeScript support
- DevTools: Use in development for debugging