react-state-management

掌握现代React状态管理,熟练运用Redux Toolkit、Zutand、Jotai和React Query。适用于配置全局状态、管理服务器状态,或在多种状态管理方案中做出选择。

查看详情
name:react-state-managementdescription: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.

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

    TypeDescriptionSolutions
    Local StateComponent-specific, UI stateuseState, useReducer
    Global StateShared across componentsRedux Toolkit, Zustand, Jotai
    Server StateRemote data, cachingReact Query, SWR, RTK Query
    URL StateRoute parameters, searchReact Router, nuqs
    Form StateInput values, validationReact 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 {
    <div class="overflow-x-auto my-6"><table class="min-w-full divide-y divide-border border border-border"><thead><tr><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">user: User</th><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">null</th></tr></thead><tbody class="divide-y divide-border"><tr><td class="px-4 py-2 text-sm text-foreground">setUser: (user: User</td><td class="px-4 py-2 text-sm text-foreground">null) =&gt; void</td></tr></tbody></table></div>
    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

    // 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 {
    <div class="overflow-x-auto my-6"><table class="min-w-full divide-y divide-border border border-border"><thead><tr><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">current: User</th><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">null</th></tr></thead><tbody class="divide-y divide-border"><tr><td class="px-4 py-2 text-sm text-foreground">error: string</td><td class="px-4 py-2 text-sm text-foreground">null</td></tr></tbody></table></div>
    }

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

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

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

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

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

  • Zustand GitHub

  • Jotai Documentation

  • TanStack Query

    1. react-state-management - Agent Skills