| name | client-state-management |
| description | Guide for implementing client-side state management in React applications. Use when building state architecture, selecting state libraries (Context, Zustand, Redux, Jotai), implementing caching strategies (React Query, SWR), optimistic updates, state persistence, or optimizing re-renders. Triggers on questions about global vs local state, state normalization, or selector patterns. |
Client-Side State Management
Decision: Library Selection
| Need | Use | Why |
|---|
| Simple shared state, <5 consumers | Context API | Zero dependencies, built-in |
| Medium complexity, performance matters | Zustand | 1.5kb, no boilerplate, auto re-render optimization |
| Large app, strict patterns needed | Redux Toolkit | DevTools, middleware ecosystem, time-travel |
| Fine-grained reactivity, atoms | Jotai | Bottom-up, minimal re-renders, composable |
| Server state (fetching/caching) | React Query or SWR | Deduplication, background refresh, cache |
Decision: Global vs Local State
Keep Local (useState/useReducer):
- Form input values before submission
- UI state (open/closed, hover, focus)
- Component-specific loading/error states
Promote to Global:
- User session/auth
- Theme/locale preferences
- Data shared across 3+ unrelated components
- State that must survive navigation
State Normalization
Flatten nested data to avoid update complexity:
{ posts: [{ id: 1, author: { id: 1, name: 'Jo' }, comments: [...] }] }
{
posts: { byId: { 1: { id: 1, authorId: 1, commentIds: [1,2] } }, allIds: [1] },
users: { byId: { 1: { id: 1, name: 'Jo' } } },
comments: { byId: { 1: {...}, 2: {...} } }
}
Optimistic Updates Pattern
Update UI immediately, rollback on error:
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], old => [...old, newTodo])
return { previous }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous)
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
})
State Persistence
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({ theme: 'light', setTheme: (t) => set({ theme: t }) }),
{
name: 'app-settings',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme })
}
)
)
Performance: Selector Patterns
Prevent unnecessary re-renders by selecting only needed state:
const count = useStore((state) => state.count)
const nameAtom = selectAtom(userAtom, (user) => user.name)
useQuery({
queryKey: ['user'],
queryFn: fetchUser,
select: (data) => data.name
})
Reference Files
Performance Checklist