| 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. |
| risk | unknown |
| source | community |
| date_added | 2026-02-27 |
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)
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' }
)
)
)
() {
{ user, theme, toggleTheme } = ()
(
)
}
Patterns
Pattern 1: Redux Toolkit with TypeScript
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
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
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()
response.()
} (error) {
((error ).)
}
}
)
userSlice = ({
: ,
initialState,
: {
: {
state. = action.
state. =
},
: {
state. =
state. =
},
},
: {
builder
.(fetchUser., {
state. =
state. =
})
.(fetchUser., {
state. =
state. = action.
})
.(fetchUser., {
state. =
state. = action.
})
},
})
{ setUser, clearUser } = userSlice.
userSlice.
Pattern 2: Zustand with Slices (Scalable)
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,
[],
[],
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 })
},
})
{ create }
{ createUserSlice, }
{ createCartSlice, }
= &
useStore = create<>()( ({
...(...args),
...(...args),
}))
= () => ( state.)
= () => ( state.)
Pattern 3: Jotai for Atomic State
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export const userAtom = atom<User | null>(null)
export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
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()
})
export const logoutAtom = atom(null, () => {
(userAtom, )
(cartAtom, [])
.()
})
() {
[user] = (userAtom)
[, logout] = (logoutAtom)
[profile] = (userProfileAtom)
(
)
}
Pattern 4: React Query for Server State
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
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,
}
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => fetchUsers(filters),
staleTime: * * ,
: * * ,
})
}
() {
({
: userKeys.(id),
: (id),
: !!id,
})
}
() {
queryClient = ()
({
: updateUser,
: (newUser) => {
queryClient.({ : userKeys.(newUser.) })
previousUser = queryClient.(userKeys.(newUser.))
queryClient.(userKeys.(newUser.), newUser)
{ previousUser }
},
: {
queryClient.(
userKeys.(newUser.),
context?.
)
},
: {
queryClient.({ : userKeys.(variables.) })
},
})
}
Pattern 5: Combining Client + Server State
const useUIStore = create<UIState>((set) => ({
sidebarOpen: true,
modal: null,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
openModal: (modal) => set({ modal }),
closeModal: () => set({ modal: null }),
}))
function Dashboard() {
const { sidebarOpen, toggleSidebar } = useUIStore()
const { data: users, isLoading } = useUsers({ active: true })
const { data: stats } = useStats()
if (isLoading) return <DashboardSkeleton />
return (
<div className={sidebarOpen ? 'with-sidebar' : ''}>
<Sidebar open= = />
)
}
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
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
}
}
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.push({ text: action.payload, completed: false })
},
},
})
Resources