git:20260307.47a5dbc to git:20260522.be57c0b

2 added, 298 removed. Audit A to A.

---
name: react-state-management
description: Master modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use when setting up global state, managing server state, or choosing between state management solutions.
---
# React State Management
Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.
## When to Use This Skill
- Setting up global state management in a React app
- Choosing between Redux Toolkit, Zustand, or Jotai
- Managing server state with React Query or SWR
- Implementing optimistic updates
- Debugging state-related issues
- Migrating from legacy Redux to modern patterns
## Core Concepts
### 1. State Categories
| Type | Description | Solutions |
| ---------------- | ---------------------------- | ----------------------------- |
| **Local State** | Component-specific, UI state | useState, useReducer |
| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |
| **Server State** | Remote data, caching | React Query, SWR, RTK Query |
| **URL State** | Route parameters, search | React Router, nuqs |
| **Form State** | Input values, validation | React Hook Form, Formik |
### 2. Selection Criteria
```
Small app, simple state → Zustand or Jotai
Large app, complex state → Redux Toolkit
Heavy server interaction → React Query + light client state
Atomic/granular updates → Jotai
```
## Quick Start
### Zustand (Simplest)
```typescript
// store/useStore.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface AppState {
user: User | null
theme: 'light' | 'dark'
setUser: (user: User | null) => void
toggleTheme: () => void
}
export const useStore = create<AppState>()(
devtools(
persist(
(set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
}),
{ name: 'app-storage' }
)
)
)
// Usage in component
function Header() {
const { user, theme, toggleTheme } = useStore()
return (
<header className={theme}>
{user?.name}
<button onClick={toggleTheme}>Toggle Theme</button>
</header>
)
}
```
- ## Patterns
-
- ### Pattern 1: Redux Toolkit with TypeScript
-
- ```typescript
- // store/index.ts
- import { configureStore } from "@reduxjs/toolkit";
- import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
- import userReducer from "./slices/userSlice";
- import cartReducer from "./slices/cartSlice";
-
- export const store = configureStore({
- reducer: {
- user: userReducer,
- cart: cartReducer,
- },
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
- serializableCheck: {
- ignoredActions: ["persist/PERSIST"],
- },
- }),
- });
-
- export type RootState = ReturnType<typeof store.getState>;
- export type AppDispatch = typeof store.dispatch;
-
- // Typed hooks
- export const useAppDispatch: () => AppDispatch = useDispatch;
- export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
- ```
-
- ```typescript
- // store/slices/userSlice.ts
- import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
-
- interface User {
- id: string;
- email: string;
- name: string;
- }
-
- interface UserState {
- current: User | null;
- status: "idle" | "loading" | "succeeded" | "failed";
- error: string | null;
- }
-
- const initialState: UserState = {
- current: null,
- status: "idle",
- error: null,
- };
-
- export const fetchUser = createAsyncThunk(
- "user/fetchUser",
- async (userId: string, { rejectWithValue }) => {
- try {
- const response = await fetch(`/api/users/${userId}`);
- if (!response.ok) throw new Error("Failed to fetch user");
- return await response.json();
- } catch (error) {
- return rejectWithValue((error as Error).message);
- }
- },
- );
-
- const userSlice = createSlice({
- name: "user",
- initialState,
- reducers: {
- setUser: (state, action: PayloadAction<User>) => {
- state.current = action.payload;
- state.status = "succeeded";
- },
- clearUser: (state) => {
- state.current = null;
- state.status = "idle";
- },
- },
- extraReducers: (builder) => {
- builder
- .addCase(fetchUser.pending, (state) => {
- state.status = "loading";
- state.error = null;
- })
- .addCase(fetchUser.fulfilled, (state, action) => {
- state.status = "succeeded";
- state.current = action.payload;
- })
- .addCase(fetchUser.rejected, (state, action) => {
- state.status = "failed";
- state.error = action.payload as string;
- });
- },
- });
-
- export const { setUser, clearUser } = userSlice.actions;
- export default userSlice.reducer;
- ```
-
- ### Pattern 2: Zustand with Slices (Scalable)
-
- ```typescript
- // store/slices/createUserSlice.ts
- import { StateCreator } from "zustand";
-
- export interface UserSlice {
- user: User | null;
- isAuthenticated: boolean;
- login: (credentials: Credentials) => Promise<void>;
- logout: () => void;
- }
-
- export const createUserSlice: StateCreator<
- UserSlice & CartSlice, // Combined store type
- [],
- [],
- UserSlice
- > = (set, get) => ({
- user: null,
- isAuthenticated: false,
- login: async (credentials) => {
- const user = await authApi.login(credentials);
- set({ user, isAuthenticated: true });
- },
- logout: () => {
- set({ user: null, isAuthenticated: false });
- // Can access other slices
- // get().clearCart()
- },
- });
-
- // store/index.ts
- import { create } from "zustand";
- import { createUserSlice, UserSlice } from "./slices/createUserSlice";
- import { createCartSlice, CartSlice } from "./slices/createCartSlice";
-
- type StoreState = UserSlice & CartSlice;
-
- export const useStore = create<StoreState>()((...args) => ({
- ...createUserSlice(...args),
- ...createCartSlice(...args),
- }));
-
- // Selective subscriptions (prevents unnecessary re-renders)
- export const useUser = () => useStore((state) => state.user);
- export const useCart = () => useStore((state) => state.cart);
- ```
-
- ### Pattern 3: Jotai for Atomic State
-
- ```typescript
- // atoms/userAtoms.ts
- import { atom } from 'jotai'
- import { atomWithStorage } from 'jotai/utils'
-
- // Basic atom
- export const userAtom = atom<User | null>(null)
-
- // Derived atom (computed)
- export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)
-
- // Atom with localStorage persistence
- export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
-
- // Async atom
- export const userProfileAtom = atom(async (get) => {
- const user = get(userAtom)
- if (!user) return null
- const response = await fetch(`/api/users/${user.id}/profile`)
- return response.json()
- })
-
- // Write-only atom (action)
- export const logoutAtom = atom(null, (get, set) => {
- set(userAtom, null)
- set(cartAtom, [])
- localStorage.removeItem('token')
- })
-
- // Usage
- function Profile() {
- const [user] = useAtom(userAtom)
- const [, logout] = useAtom(logoutAtom)
- const [profile] = useAtom(userProfileAtom) // Suspense-enabled
-
- return (
- <Suspense fallback={<Skeleton />}>
- <ProfileContent profile={profile} onLogout={logout} />
- </Suspense>
- )
- }
- ```
-
- ### Pattern 4: React Query for Server State
-
- ```typescript
- // hooks/useUsers.ts
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-
- // Query keys factory
- 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 hook
- export function useUsers(filters: UserFilters) {
- return useQuery({
- queryKey: userKeys.list(filters),
- queryFn: () => fetchUsers(filters),
- staleTime: 5 * 60 * 1000, // 5 minutes
- gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
- });
- }
-
- // Single user hook
- export function useUser(id: string) {
- return useQuery({
- queryKey: userKeys.detail(id),
- queryFn: () => fetchUser(id),
- enabled: !!id, // Don't fetch if no id
- });
- }
-
- // Mutation with optimistic update
- export function useUpdateUser() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: updateUser,
- onMutate: async (newUser) => {
- // Cancel outgoing refetches
- await queryClient.cancelQueries({
- queryKey: userKeys.detail(newUser.id),
- });
-
- // Snapshot previous value
- const previousUser = queryClient.getQueryData(
- userKeys.detail(newUser.id),
- );
-
- // Optimistically update
- queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
-
- return { previousUser };
- },
- onError: (err, newUser, context) => {
- // Rollback on error
- queryClient.setQueryData(
- userKeys.detail(newUser.id),
- context?.previousUser,
- );
- },
- onSettled: (data, error, variables) => {
- // Refetch after mutation
- queryClient.invalidateQueries({
- queryKey: userKeys.detail(variables.id),
- });
- },
- });
- }
- ```
-
- ### Pattern 5: Combining Client + Server State
-
- ```typescript
- // Zustand for client state
- const useUIStore = create<UIState>((set) => ({
- sidebarOpen: true,
- modal: null,
- toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
- openModal: (modal) => set({ modal }),
- closeModal: () => set({ modal: null }),
- }))
-
- // React Query for server state
- function Dashboard() {
- const { sidebarOpen, toggleSidebar } = useUIStore()
- const { data: users, isLoading } = useUsers({ active: true })
- const { data: stats } = useStats()
-
- if (isLoading) return <DashboardSkeleton />
+ ## Detailed patterns and worked examples
- return (
- <div className={sidebarOpen ? 'with-sidebar' : ''}>
- <Sidebar open={sidebarOpen} onToggle={toggleSidebar} />
- <main>
- <StatsCards stats={stats} />
- <UserTable users={users} />
- </main>
- </div>
- )
- }
- ```
+ Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
## Best Practices
### Do's
- **Colocate state** - Keep state as close to where it's used as possible
- **Use selectors** - Prevent unnecessary re-renders with selective subscriptions
- **Normalize data** - Flatten nested structures for easier updates
- **Type everything** - Full TypeScript coverage prevents runtime errors
- **Separate concerns** - Server state (React Query) vs client state (Zustand)
### Don'ts
- **Don't over-globalize** - Not everything needs to be in global state
- **Don't duplicate server state** - Let React Query manage it
- **Don't mutate directly** - Always use immutable updates
- **Don't store derived data** - Compute it instead
- **Don't mix paradigms** - Pick one primary solution per category
## Migration Guides
### From Legacy Redux to RTK
```typescript
// Before (legacy Redux)
const ADD_TODO = "ADD_TODO";
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, { text: action.payload, completed: false }];
default:
return state;
}
}
// After (Redux Toolkit)
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
// Immer allows "mutations"
state.push({ text: action.payload, completed: false });
},
},
});
```