React State Management
Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.
Do not use this skill when
- The task is unrelated to react state management
- You need a different domain or tool outside this scope
Instructions
- Clarify goals, constraints, and required inputs.
- Apply relevant best practices and validate outcomes.
- Provide actionable steps and verification.
- If detailed examples are required, open
resources/implementation-playbook.md.
Use this skill when
- 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)
// 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 Theme</button>
</header>
)
}
Patterns
Pattern 1: Redux Toolkit with 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
// 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)
// store/slices/createUserSlice.ts
import { StateCreator }
1---2name: react-state-management3description: 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.4---567# React State Management89Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.1011## Do not use this skill when1213- The task is unrelated to react state management14- You need a different domain or tool outside this scope1516## Instructions1718- Clarify goals, constraints, and required inputs.19- Apply relevant best practices and validate outcomes.20- Provide actionable steps and verification.21- If detailed examples are required, open `resources/implementation-playbook.md`.2223## Use this skill when2425- Setting up global state management in a React app26- Choosing between Redux Toolkit, Zustand, or Jotai27- Managing server state with React Query or SWR28- Implementing optimistic updates29- Debugging state-related issues30- Migrating from legacy Redux to modern patterns3132## Core Concepts3334### 1. State Categories3536| Type | Description | Solutions |37|------|-------------|-----------|38| **Local State** | Component-specific, UI state | useState, useReducer |39| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |40| **Server State** | Remote data, caching | React Query, SWR, RTK Query |41| **URL State** | Route parameters, search | React Router, nuqs |42| **Form State** | Input values, validation | React Hook Form, Formik |4344### 2. Selection Criteria4546```47Small app, simple state → Zustand or Jotai48Large app, complex state → Redux Toolkit49Heavy server interaction → React Query + light client state50Atomic/granular updates → Jotai51```5253## Quick Start5455### Zustand (Simplest)5657```typescript58// store/useStore.ts59import { create } from 'zustand'60import { devtools, persist } from 'zustand/middleware'6162interface AppState {63 user: User | null64 theme: 'light' | 'dark'65 setUser: (user: User | null) => void66 toggleTheme: () => void67}6869export const useStore = create<AppState>()(70 devtools(71 persist(72 (set) => ({73 user: null,74 theme: 'light',75 setUser: (user) => set({ user }),76 toggleTheme: () => set((state) => ({77 theme: state.theme === 'light' ? 'dark' : 'light'78 })),79 }),80 { name: 'app-storage' }81 )82 )83)8485// Usage in component86function Header() {87 const { user, theme, toggleTheme } = useStore()88 return (89 <header className={theme}>90 {user?.name}91 <button onClick={toggleTheme}>Toggle Theme</button>92 </header>93 )94}95```9697## Patterns9899### Pattern 1: Redux Toolkit with TypeScript100101```typescript102// store/index.ts103import { configureStore } from '@reduxjs/toolkit'104import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'105import userReducer from './slices/userSlice'106import cartReducer from './slices/cartSlice'107108export const store = configureStore({109 reducer: {110 user: userReducer,111 cart: cartReducer,112 },113 middleware: (getDefaultMiddleware) =>114 getDefaultMiddleware({115 serializableCheck: {116 ignoredActions: ['persist/PERSIST'],117 },118 }),119})120121export type RootState = ReturnType<typeof store.getState>122export type AppDispatch = typeof store.dispatch123124// Typed hooks125export const useAppDispatch: () => AppDispatch = useDispatch126export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector127```128129```typescript130// store/slices/userSlice.ts131import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'132133interface User {134 id: string135 email: string136 name: string137}138139interface UserState {140 current: User | null141 status: 'idle' | 'loading' | 'succeeded' | 'failed'142 error: string | null143}144145const initialState: UserState = {146 current: null,147 status: 'idle',148 error: null,149}150151export const fetchUser = createAsyncThunk(152 'user/fetchUser',153 async (userId: string, { rejectWithValue }) => {154 try {155 const response = await fetch(`/api/users/${userId}`)156 if (!response.ok) throw new Error('Failed to fetch user')157 return await response.json()158 } catch (error) {159 return rejectWithValue((error as Error).message)160 }161 }162)163164const userSlice = createSlice({165 name: 'user',166 initialState,167 reducers: {168 setUser: (state, action: PayloadAction<User>) => {169 state.current = action.payload170 state.status = 'succeeded'171 },172 clearUser: (state) => {173 state.current = null174 state.status = 'idle'175 },176 },177 extraReducers: (builder) => {178 builder179 .addCase(fetchUser.pending, (state) => {180 state.status = 'loading'181 state.error = null182 })183 .addCase(fetchUser.fulfilled, (state, action) => {184 state.status = 'succeeded'185 state.current = action.payload186 })187 .addCase(fetchUser.rejected, (state, action) => {188 state.status = 'failed'189 state.error = action.payload as string190 })191 },192})193194export const { setUser, clearUser } = userSlice.actions195export default userSlice.reducer196```197198### Pattern 2: Zustand with Slices (Scalable)199200```typescript201// store/slices/createUserSlice.ts202import { StateCreator }