# React State Management

> 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.

- Skill: `involvex/react-state-management-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add involvex/react-state-management-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/involvex/react-state-management-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: involvex (https://skillmd.com/u/involvex)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/involvex/react-state-management-2

---


# 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)

```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 {TypedUseSelectorHook, useDispatch, useSelector} from 'react-redux'
import {configureStore} from '@reduxjs/toolkit'
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 {createUserSlice, UserSlice} from './slices/createUserSlice'
import {createCartSlice, CartSlice} from './slices/createCartSlice'
import {create} from 'zustand'

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 />

  return (
    <div className={sidebarOpen ? 'with-sidebar' : ''}>
      <Sidebar open={sidebarOpen} onToggle={toggleSidebar} />
      <main>
        <StatsCards stats={stats} />
        <UserTable users={users} />
      </main>
    </div>
  )
}
```

## 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})
		},
	},
})
```

## Resources

- [Redux Toolkit Documentation](https://redux-toolkit.js.org/)
- [Zustand GitHub](https://github.com/pmndrs/zustand)
- [Jotai Documentation](https://jotai.org/)
- [TanStack Query](https://tanstack.com/query)

