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.
Add this skill
npx mdskills install sickn33/react-state-managementComprehensive guide with actionable patterns for Redux Toolkit, Zustand, Jotai, and React Query
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---56# React State Management78Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.910## Do not use this skill when1112- The task is unrelated to react state management13- You need a different domain or tool outside this scope1415## Instructions1617- Clarify goals, constraints, and required inputs.18- Apply relevant best practices and validate outcomes.19- Provide actionable steps and verification.20- If detailed examples are required, open `resources/implementation-playbook.md`.2122## Use this skill when2324- Setting up global state management in a React app25- Choosing between Redux Toolkit, Zustand, or Jotai26- Managing server state with React Query or SWR27- Implementing optimistic updates28- Debugging state-related issues29- Migrating from legacy Redux to modern patterns3031## Core Concepts3233### 1. State Categories3435| Type | Description | Solutions |36|------|-------------|-----------|37| **Local State** | Component-specific, UI state | useState, useReducer |38| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |39| **Server State** | Remote data, caching | React Query, SWR, RTK Query |40| **URL State** | Route parameters, search | React Router, nuqs |41| **Form State** | Input values, validation | React Hook Form, Formik |4243### 2. Selection Criteria4445```46Small app, simple state → Zustand or Jotai47Large app, complex state → Redux Toolkit48Heavy server interaction → React Query + light client state49Atomic/granular updates → Jotai50```5152## Quick Start5354### Zustand (Simplest)5556```typescript57// store/useStore.ts58import { create } from 'zustand'59import { devtools, persist } from 'zustand/middleware'6061interface AppState {62 user: User | null63 theme: 'light' | 'dark'64 setUser: (user: User | null) => void65 toggleTheme: () => void66}6768export const useStore = create<AppState>()(69 devtools(70 persist(71 (set) => ({72 user: null,73 theme: 'light',74 setUser: (user) => set({ user }),75 toggleTheme: () => set((state) => ({76 theme: state.theme === 'light' ? 'dark' : 'light'77 })),78 }),79 { name: 'app-storage' }80 )81 )82)8384// Usage in component85function Header() {86 const { user, theme, toggleTheme } = useStore()87 return (88 <header className={theme}>89 {user?.name}90 <button onClick={toggleTheme}>Toggle Theme</button>91 </header>92 )93}94```9596## Patterns9798### Pattern 1: Redux Toolkit with TypeScript99100```typescript101// store/index.ts102import { configureStore } from '@reduxjs/toolkit'103import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'104import userReducer from './slices/userSlice'105import cartReducer from './slices/cartSlice'106107export const store = configureStore({108 reducer: {109 user: userReducer,110 cart: cartReducer,111 },112 middleware: (getDefaultMiddleware) =>113 getDefaultMiddleware({114 serializableCheck: {115 ignoredActions: ['persist/PERSIST'],116 },117 }),118})119120export type RootState = ReturnType<typeof store.getState>121export type AppDispatch = typeof store.dispatch122123// Typed hooks124export const useAppDispatch: () => AppDispatch = useDispatch125export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector126```127128```typescript129// store/slices/userSlice.ts130import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'131132interface User {133 id: string134 email: string135 name: string136}137138interface UserState {139 current: User | null140 status: 'idle' | 'loading' | 'succeeded' | 'failed'141 error: string | null142}143144const initialState: UserState = {145 current: null,146 status: 'idle',147 error: null,148}149150export const fetchUser = createAsyncThunk(151 'user/fetchUser',152 async (userId: string, { rejectWithValue }) => {153 try {154 const response = await fetch(`/api/users/${userId}`)155 if (!response.ok) throw new Error('Failed to fetch user')156 return await response.json()157 } catch (error) {158 return rejectWithValue((error as Error).message)159 }160 }161)162163const userSlice = createSlice({164 name: 'user',165 initialState,166 reducers: {167 setUser: (state, action: PayloadAction<User>) => {168 state.current = action.payload169 state.status = 'succeeded'170 },171 clearUser: (state) => {172 state.current = null173 state.status = 'idle'174 },175 },176 extraReducers: (builder) => {177 builder178 .addCase(fetchUser.pending, (state) => {179 state.status = 'loading'180 state.error = null181 })182 .addCase(fetchUser.fulfilled, (state, action) => {183 state.status = 'succeeded'184 state.current = action.payload185 })186 .addCase(fetchUser.rejected, (state, action) => {187 state.status = 'failed'188 state.error = action.payload as string189 })190 },191})192193export const { setUser, clearUser } = userSlice.actions194export default userSlice.reducer195```196197### Pattern 2: Zustand with Slices (Scalable)198199```typescript200// store/slices/createUserSlice.ts201import { StateCreator } from 'zustand'202203export interface UserSlice {204 user: User | null205 isAuthenticated: boolean206 login: (credentials: Credentials) => Promise<void>207 logout: () => void208}209210export const createUserSlice: StateCreator<211 UserSlice & CartSlice, // Combined store type212 [],213 [],214 UserSlice215> = (set, get) => ({216 user: null,217 isAuthenticated: false,218 login: async (credentials) => {219 const user = await authApi.login(credentials)220 set({ user, isAuthenticated: true })221 },222 logout: () => {223 set({ user: null, isAuthenticated: false })224 // Can access other slices225 // get().clearCart()226 },227})228229// store/index.ts230import { create } from 'zustand'231import { createUserSlice, UserSlice } from './slices/createUserSlice'232import { createCartSlice, CartSlice } from './slices/createCartSlice'233234type StoreState = UserSlice & CartSlice235236export const useStore = create<StoreState>()((...args) => ({237 ...createUserSlice(...args),238 ...createCartSlice(...args),239}))240241// Selective subscriptions (prevents unnecessary re-renders)242export const useUser = () => useStore((state) => state.user)243export const useCart = () => useStore((state) => state.cart)244```245246### Pattern 3: Jotai for Atomic State247248```typescript249// atoms/userAtoms.ts250import { atom } from 'jotai'251import { atomWithStorage } from 'jotai/utils'252253// Basic atom254export const userAtom = atom<User | null>(null)255256// Derived atom (computed)257export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)258259// Atom with localStorage persistence260export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')261262// Async atom263export const userProfileAtom = atom(async (get) => {264 const user = get(userAtom)265 if (!user) return null266 const response = await fetch(`/api/users/${user.id}/profile`)267 return response.json()268})269270// Write-only atom (action)271export const logoutAtom = atom(null, (get, set) => {272 set(userAtom, null)273 set(cartAtom, [])274 localStorage.removeItem('token')275})276277// Usage278function Profile() {279 const [user] = useAtom(userAtom)280 const [, logout] = useAtom(logoutAtom)281 const [profile] = useAtom(userProfileAtom) // Suspense-enabled282283 return (284 <Suspense fallback={<Skeleton />}>285 <ProfileContent profile={profile} onLogout={logout} />286 </Suspense>287 )288}289```290291### Pattern 4: React Query for Server State292293```typescript294// hooks/useUsers.ts295import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'296297// Query keys factory298export const userKeys = {299 all: ['users'] as const,300 lists: () => [...userKeys.all, 'list'] as const,301 list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,302 details: () => [...userKeys.all, 'detail'] as const,303 detail: (id: string) => [...userKeys.details(), id] as const,304}305306// Fetch hook307export function useUsers(filters: UserFilters) {308 return useQuery({309 queryKey: userKeys.list(filters),310 queryFn: () => fetchUsers(filters),311 staleTime: 5 * 60 * 1000, // 5 minutes312 gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)313 })314}315316// Single user hook317export function useUser(id: string) {318 return useQuery({319 queryKey: userKeys.detail(id),320 queryFn: () => fetchUser(id),321 enabled: !!id, // Don't fetch if no id322 })323}324325// Mutation with optimistic update326export function useUpdateUser() {327 const queryClient = useQueryClient()328329 return useMutation({330 mutationFn: updateUser,331 onMutate: async (newUser) => {332 // Cancel outgoing refetches333 await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })334335 // Snapshot previous value336 const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id))337338 // Optimistically update339 queryClient.setQueryData(userKeys.detail(newUser.id), newUser)340341 return { previousUser }342 },343 onError: (err, newUser, context) => {344 // Rollback on error345 queryClient.setQueryData(346 userKeys.detail(newUser.id),347 context?.previousUser348 )349 },350 onSettled: (data, error, variables) => {351 // Refetch after mutation352 queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })353 },354 })355}356```357358### Pattern 5: Combining Client + Server State359360```typescript361// Zustand for client state362const useUIStore = create<UIState>((set) => ({363 sidebarOpen: true,364 modal: null,365 toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),366 openModal: (modal) => set({ modal }),367 closeModal: () => set({ modal: null }),368}))369370// React Query for server state371function Dashboard() {372 const { sidebarOpen, toggleSidebar } = useUIStore()373 const { data: users, isLoading } = useUsers({ active: true })374 const { data: stats } = useStats()375376 if (isLoading) return <DashboardSkeleton />377378 return (379 <div className={sidebarOpen ? 'with-sidebar' : ''}>380 <Sidebar open={sidebarOpen} onToggle={toggleSidebar} />381 <main>382 <StatsCards stats={stats} />383 <UserTable users={users} />384 </main>385 </div>386 )387}388```389390## Best Practices391392### Do's393- **Colocate state** - Keep state as close to where it's used as possible394- **Use selectors** - Prevent unnecessary re-renders with selective subscriptions395- **Normalize data** - Flatten nested structures for easier updates396- **Type everything** - Full TypeScript coverage prevents runtime errors397- **Separate concerns** - Server state (React Query) vs client state (Zustand)398399### Don'ts400- **Don't over-globalize** - Not everything needs to be in global state401- **Don't duplicate server state** - Let React Query manage it402- **Don't mutate directly** - Always use immutable updates403- **Don't store derived data** - Compute it instead404- **Don't mix paradigms** - Pick one primary solution per category405406## Migration Guides407408### From Legacy Redux to RTK409410```typescript411// Before (legacy Redux)412const ADD_TODO = 'ADD_TODO'413const addTodo = (text) => ({ type: ADD_TODO, payload: text })414function todosReducer(state = [], action) {415 switch (action.type) {416 case ADD_TODO:417 return [...state, { text: action.payload, completed: false }]418 default:419 return state420 }421}422423// After (Redux Toolkit)424const todosSlice = createSlice({425 name: 'todos',426 initialState: [],427 reducers: {428 addTodo: (state, action: PayloadAction<string>) => {429 // Immer allows "mutations"430 state.push({ text: action.payload, completed: false })431 },432 },433})434```435436## Resources437438- [Redux Toolkit Documentation](https://redux-toolkit.js.org/)439- [Zustand GitHub](https://github.com/pmndrs/zustand)440- [Jotai Documentation](https://jotai.org/)441- [TanStack Query](https://tanstack.com/query)442
Full transparency — inspect the skill content before installing.