Next.js 状态管理

本轮先交付一个状态归属练习

前置:已有能启动的 Next.js App Router + TypeScript 项目,知道客户端组件与事件处理器。本文包含多个独立片段,Context、Zustand、查询库与表单部分不是一套可直接拼接的应用;先只接入下文 Counter,暂不安装其他状态库。

产物:一个 /state-lab 页面、Counter 组件和一张操作记录。页面作为客户端组件,渲染 <Counter initialValue={1} min={0} max={2} onChange={setLast} />,在父组件用 useState<number | null>(null) 保存 last 并显示它。这能观察“子组件拥有 count、父组件收到通知”的边界。若父组件也必须控制 count,则改为 value/onChange 受控接口,只保留一个拥有者,参见 共享状态。

输入操作预期界面输出失败时回查
初次进入count 为 1,last 为 null是否在挂载或渲染期间错误调用回调
点击加号一次count 与 last 都为 2,加号禁用是否把 onChange 留在 updater 中
连点减号直到下限停在 0,减号禁用边界是否同时作用于按钮与计算
点击重置两处显示 1reset 是否使用同一个初始值
浏览器刷新count 回到 1,last 回到 null本地内存状态本就不保证持久化

这些是待执行的验收预期。本轮未启动 Next.js、未执行浏览器交互或生产构建,不能据此宣称框架项目通过。执行时记录 Node/Next/React 版本、锁文件与浏览器结果;项目使用 npm 时先运行既有的 npm run dev,再运行 npm run build,分别记录开发交互与构建结果。

下一步:把“已提交搜索词”迁入 URL,再用刷新和浏览器后退检查恢复;保留输入草稿在组件内。先写出草稿、URL 和远端结果各由谁拥有,再决定是否引入 Context 或查询库。

状态按拥有者分层:输入框文本在本地,分享筛选在 URL,服务器记录以远端为准。复制一份远端数据到全局 store 会增加同步责任。

状态管理核心概念

  • 理解不同类型的状态:本地状态、全局状态、服务器状态、表单状态
  • 掌握 React 19的状态管理最佳实践
  • 学会选择合适的状态管理解决方案
  • 理解状态持久化和同步机制

实际应用能力

  • 构建可扩展的状态架构
  • 实现状态的性能优化和调试
  • 掌握复杂业务逻辑的状态处理
  • 学会状态测试和验证

概述

Next.js 16应用中的状态管理需要考虑服务器端和客户端的不同特点。本教程将帮助你掌握现代 React 状态管理的核心概念,学会选择和实现适合不同场景的状态管理解决方案。

状态管理架构概览

状态分类体系

// src/types/state-architecture.ts
export interface StateArchitecture {
  // 本地状态
  localState: {
    componentState: '组件内部状态'
    formState: '表单状态'
    uiState: 'UI交互状态'
  }

  // 全局状态
  globalState: {
    userState: '用户认证状态'
    appConfig: '应用配置状态'
    themeState: '主题和偏好设置'
  }

  // 服务器状态
  serverState: {
    apiData: 'API获取的数据'
    cacheData: '缓存的数据'
    realtimeData: '实时数据'
  }

  // URL状态
  urlState: {
    queryParams: '查询参数状态'
    routeParams: '路由参数状态'
    hashState: 'Hash状态'
  }
}

本地状态管理

组件状态基础

// src/components/Counter.tsx
'use client'

import { useState, useCallback } from 'react'

interface CounterProps {
  initialValue?: number
  max?: number
  min?: number
  onChange?: (value: number) => void
}

export function Counter({
  initialValue = 0,
  max = 100,
  min = 0,
  onChange
}: CounterProps) {
  const [count, setCount] = useState(initialValue)

  const increment = useCallback(() => {
    const next = Math.min(count + 1, max)
    setCount(next)
    onChange?.(next)
  }, [count, max, onChange])

  const decrement = useCallback(() => {
    const next = Math.max(count - 1, min)
    setCount(next)
    onChange?.(next)
  }, [count, min, onChange])

  const reset = useCallback(() => {
    setCount(initialValue)
    onChange?.(initialValue)
  }, [initialValue, onChange])

  return (
    <div className="flex items-center space-x-4">
      <button
        onClick={decrement}
        disabled={count <= min}
        className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        -
      </button>

      <span className="text-xl font-semibold w-12 text-center">
        {count}
      </span>

      <button
        onClick={increment}
        disabled={count >= max}
        className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        +
      </button>

      <button
        onClick={reset}
        className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        重置
      </button>
    </div>
  )
}

复杂组件状态

上面的按钮每次用户事件只执行一次状态转换,回调放在事件处理器里。不要把通知父组件、写存储或发请求放进 updater:updater 必须纯净,开发环境 Strict Mode 可能重复调用它。若同一事件要连续累加三次,应使用三个纯 updater,或者一次计算最终值;不要把本例的 increment() 连调三次当作 +3。initialValue 只初始化首次挂载,后续 props 改变不会自动重置 count。参见 React useState。

// src/components/TodoList.tsx
'use client'

import { useState, useCallback, useMemo } from 'react'
import { v4 as uuidv4 } from 'uuid'

interface Todo {
  id: string
  text: string
  completed: boolean
  createdAt: Date
  completedAt?: Date
}

interface TodoListProps {
  initialTodos?: Todo[]
}

export function TodoList({ initialTodos = [] }: TodoListProps) {
  const [todos, setTodos] = useState<Todo[]>(initialTodos)
  const [newTodoText, setNewTodoText] = useState('')
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')

  // 添加新任务
  const addTodo = useCallback(() => {
    if (!newTodoText.trim()) return

    const newTodo: Todo = {
      id: uuidv4(),
      text: newTodoText.trim(),
      completed: false,
      createdAt: new Date()
    }

    setTodos(prev => [...prev, newTodo])
    setNewTodoText('')
  }, [newTodoText])

  // 切换任务状态
  const toggleTodo = useCallback((id: string) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id
        ? {
            ...todo,
            completed: !todo.completed,
            completedAt: !todo.completed ? new Date() : undefined
          }
        : todo
    ))
  }, [])

  // 删除任务
  const deleteTodo = useCallback((id: string) => {
    setTodos(prev => prev.filter(todo => todo.id !== id))
  }, [])

  // 编辑任务
  const editTodo = useCallback((id: string, newText: string) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, text: newText } : todo
    ))
  }, [])

  // 过滤后的任务
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed)
      case 'completed':
        return todos.filter(todo => todo.completed)
      default:
        return todos
    }
  }, [todos, filter])

  // 统计信息
  const stats = useMemo(() => ({
    total: todos.length,
    active: todos.filter(todo => !todo.completed).length,
    completed: todos.filter(todo => todo.completed).length
  }), [todos])

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">待办事项</h1>

      {/* 添加新任务 */}
      <div className="flex space-x-2 mb-6">
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="添加新任务..."
          className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
        />
        <button
          onClick={addTodo}
          disabled={!newTodoText.trim()}
          className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          添加
        </button>
      </div>

      {/* 过滤器 */}
      <div className="flex space-x-2 mb-6">
        {(['all', 'active', 'completed'] as const).map(filterType => (
          <button
            key={filterType}
            onClick={() => setFilter(filterType)}
            className={`px-4 py-2 rounded-lg ${
              filter === filterType
                ? 'bg-blue-500 text-white'
                : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
            }`}
          >
            {filterType === 'all' && `全部 (${stats.total})`}
            {filterType === 'active' && `进行中 (${stats.active})`}
            {filterType === 'completed' && `已完成 (${stats.completed})`}
          </button>
        ))}
      </div>

      {/* 任务列表 */}
      <div className="space-y-2">
        {filteredTodos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
            onEdit={editTodo}
          />
        ))}
      </div>

      {filteredTodos.length === 0 && (
        <div className="text-center py-8 text-gray-500">
          {filter === 'all' && '暂无任务'}
          {filter === 'active' && '暂无进行中的任务'}
          {filter === 'completed' && '暂无已完成的任务'}
        </div>
      )}
    </div>
  )
}

// 任务项组件
interface TodoItemProps {
  todo: Todo
  onToggle: (id: string) => void
  onDelete: (id: string) => void
  onEdit: (id: string, text: string) => void
}

function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
  const [isEditing, setIsEditing] = useState(false)
  const [editText, setEditText] = useState(todo.text)

  const saveEdit = () => {
    if (editText.trim() && editText !== todo.text) {
      onEdit(todo.id, editText.trim())
    }
    setIsEditing(false)
  }

  const cancelEdit = () => {
    setEditText(todo.text)
    setIsEditing(false)
  }

  return (
    <div className="flex items-center space-x-3 p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
      />

      <div className="flex-1">
        {isEditing ? (
          <input
            type="text"
            value={editText}
            onChange={(e) => setEditText(e.target.value)}
            onKeyPress={(e) => {
              if (e.key === 'Enter') saveEdit()
              if (e.key === 'Escape') cancelEdit()
            }}
            onBlur={saveEdit}
            className="w-full px-2 py-1 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
            autoFocus
          />
        ) : (
          <span
            className={`
              cursor-text
              ${todo.completed ? 'line-through text-gray-500' : 'text-gray-900'}
            `}
            onClick={() => setIsEditing(true)}
          >
            {todo.text}
          </span>
        )}
      </div>

      <div className="text-sm text-gray-500">
        {todo.completed && todo.completedAt && (
          <span>完成于 {todo.completedAt.toLocaleDateString()}</span>
        )}
      </div>

      <button
        onClick={() => onDelete(todo.id)}
        className="px-2 py-1 text-red-600 hover:bg-red-50 rounded"
      >
        删除
      </button>
    </div>
  )
}

全局状态管理

Context API 基础

// src/contexts/AuthContext.tsx
'use client'

import React, { createContext, useContext, useReducer, useEffect } from 'react'

// 用户类型
interface User {
  id: string
  name: string
  email: string
  avatar?: string
  role: 'admin' | 'user'
}

// 认证状态
interface AuthState {
  user: User | null
  isAuthenticated: boolean
  loading: boolean
  error: string | null
}

// 认证动作
type AuthAction =
  | { type: 'AUTH_START' }
  | { type: 'AUTH_SUCCESS'; payload: User }
  | { type: 'AUTH_ERROR'; payload: string }
  | { type: 'LOGOUT' }
  | { type: 'CLEAR_ERROR' }

// 初始状态
const initialState: AuthState = {
  user: null,
  isAuthenticated: false,
  loading: false,
  error: null
}

// Reducer
function authReducer(state: AuthState, action: AuthAction): AuthState {
  switch (action.type) {
    case 'AUTH_START':
      return {
        ...state,
        loading: true,
        error: null
      }
    case 'AUTH_SUCCESS':
      return {
        ...state,
        loading: false,
        user: action.payload,
        isAuthenticated: true,
        error: null
      }
    case 'AUTH_ERROR':
      return {
        ...state,
        loading: false,
        user: null,
        isAuthenticated: false,
        error: action.payload
      }
    case 'LOGOUT':
      return {
        ...state,
        user: null,
        isAuthenticated: false,
        loading: false,
        error: null
      }
    case 'CLEAR_ERROR':
      return {
        ...state,
        error: null
      }
    default:
      return state
  }
}

// Context
const AuthContext = createContext<{
  state: AuthState
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  clearError: () => void
} | null>(null)

// Provider组件
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(authReducer, initialState)

  // 登录函数
  const login = async (email: string, password: string) => {
    dispatch({ type: 'AUTH_START' })

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      })

      const data = await response.json()

      if (!response.ok) {
        throw new Error(data.error || '登录失败')
      }

      dispatch({ type: 'AUTH_SUCCESS', payload: data.user })
    } catch (error) {
      dispatch({
        type: 'AUTH_ERROR',
        payload: error instanceof Error ? error.message : '未知错误'
      })
    }
  }

  // 登出函数
  const logout = () => {
    dispatch({ type: 'LOGOUT' })
  }

  // 清除错误
  const clearError = () => {
    dispatch({ type: 'CLEAR_ERROR' })
  }

  // 检查本地存储的认证状态
  useEffect(() => {
    const savedUser = localStorage.getItem('user')
    if (savedUser) {
      try {
        const user = JSON.parse(savedUser)
        dispatch({ type: 'AUTH_SUCCESS', payload: user })
      } catch {
        localStorage.removeItem('user')
      }
    }
  }, [])

  // 保存用户信息到本地存储
  useEffect(() => {
    if (state.user) {
      localStorage.setItem('user', JSON.stringify(state.user))
    } else {
      localStorage.removeItem('user')
    }
  }, [state.user])

  return (
    <AuthContext.Provider value={{ state, login, logout, clearError }}>
      {children}
    </AuthContext.Provider>
  )
}

// Hook
export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

Zustand 状态管理

// src/stores/useUserStore.ts
import { create } from 'zustand'
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// 用户类型
interface User {
  id: string
  name: string
  email: string
  avatar?: string
  role: 'admin' | 'user'
  preferences: {
    theme: 'light' | 'dark' | 'system'
    language: string
    notifications: boolean
  }
}

// 用户状态接口
interface UserState {
  // 状态
  user: User | null
  isAuthenticated: boolean
  loading: boolean
  error: string | null

  // 动作
  setUser: (user: User) => void
  updateUser: (updates: Partial<User>) => void
  updatePreferences: (preferences: Partial<User['preferences']>) => void
  setLoading: (loading: boolean) => void
  setError: (error: string | null) => void
  clearError: () => void
  logout: () => void

  // 异步动作
  login: (email: string, password: string) => Promise<void>
  fetchUser: () => Promise<void>
  updateProfile: (updates: Partial<User>) => Promise<void>
}

// 创建store
export const useUserStore = create<UserState>()(
  devtools(
    subscribeWithSelector(
      persist(
        immer((set, get) => ({
          // 初始状态
          user: null,
          isAuthenticated: false,
          loading: false,
          error: null,

          // 设置用户
          setUser: (user) => {
            set((state) => {
              state.user = user
              state.isAuthenticated = true
              state.error = null
            })
          },

          // 更新用户信息
          updateUser: (updates) => {
            set((state) => {
              if (state.user) {
                Object.assign(state.user, updates)
              }
            })
          },

          // 更新用户偏好
          updatePreferences: (preferences) => {
            set((state) => {
              if (state.user) {
                Object.assign(state.user.preferences, preferences)
              }
            })
          },

          // 设置加载状态
          setLoading: (loading) => {
            set((state) => {
              state.loading = loading
            })
          },

          // 设置错误
          setError: (error) => {
            set((state) => {
              state.error = error
              state.loading = false
            })
          },

          // 清除错误
          clearError: () => {
            set((state) => {
              state.error = null
            })
          },

          // 登出
          logout: () => {
            set((state) => {
              state.user = null
              state.isAuthenticated = false
              state.error = null
              state.loading = false
            })
          },

          // 登录
          login: async (email, password) => {
            const { setLoading, setError, setUser } = get()

            setLoading(true)
            setError(null)

            try {
              const response = await fetch('/api/auth/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email, password })
              })

              const data = await response.json()

              if (!response.ok) {
                throw new Error(data.error || '登录失败')
              }

              setUser(data.user)
            } catch (error) {
              setError(error instanceof Error ? error.message : '登录失败')
            } finally {
              setLoading(false)
            }
          },

          // 获取用户信息
          fetchUser: async () => {
            const { setLoading, setError, setUser } = get()

            setLoading(true)

            try {
              const response = await fetch('/api/user/profile')

              if (!response.ok) {
                throw new Error('获取用户信息失败')
              }

              const data = await response.json()
              setUser(data.user)
            } catch (error) {
              setError(error instanceof Error ? error.message : '获取用户信息失败')
            } finally {
              setLoading(false)
            }
          },

          // 更新个人资料
          updateProfile: async (updates) => {
            const { setLoading, setError, updateUser } = get()

            setLoading(true)

            try {
              const response = await fetch('/api/user/profile', {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(updates)
              })

              const data = await response.json()

              if (!response.ok) {
                throw new Error(data.error || '更新失败')
              }

              updateUser(data.user)
            } catch (error) {
              setError(error instanceof Error ? error.message : '更新失败')
            } finally {
              setLoading(false)
            }
          }
        })),
        {
          name: 'user-store',
          partialize: (state) => ({
            user: state.user,
            isAuthenticated: state.isAuthenticated
          })
        }
      )
    ),
    {
      name: 'user-store'
    }
  )
)

// 选择器hooks
export const useUser = () => useUserStore((state) => state.user)
export const useIsAuthenticated = () => useUserStore((state) => state.isAuthenticated)
export const useUserLoading = () => useUserStore((state) => state.loading)
export const useUserError = () => useUserStore((state) => state.error)
export const useUserPreferences = () => useUserStore((state) => state.user?.preferences)

服务器状态管理

React Query 集成

// src/hooks/useApiQuery.ts
'use client'

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useUserStore } from '@/stores/useUserStore'

// API查询配置
const defaultQueryConfig = {
  staleTime: 5 * 60 * 1000, // 5分钟
  cacheTime: 10 * 60 * 1000, // 10分钟
  retry: 3,
  retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000)
}

// 通用查询Hook
export function useApiQuery<T>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  config = {}
) {
  const user = useUserStore(state => state.user)

  return useQuery({
    queryKey,
    queryFn,
    ...defaultQueryConfig,
    ...config,
    enabled: !!user // 只有用户登录后才执行查询
  })
}

// 文章查询Hook
export function usePosts(params: { page?: number; category?: string } = {}) {
  return useApiQuery(
    ['posts', params],
    () => fetchPosts(params),
    {
      staleTime: 2 * 60 * 1000 // 2分钟
    }
  )
}

async function fetchPosts(params: { page?: number; category?: string }) {
  const searchParams = new URLSearchParams()
  if (params.page) searchParams.set('page', params.page.toString())
  if (params.category) searchParams.set('category', params.category)

  const response = await fetch(`/api/posts?${searchParams}`)
  if (!response.ok) throw new Error('Failed to fetch posts')
  return response.json()
}

// 单篇文章查询Hook
export function usePost(id: string) {
  return useApiQuery(
    ['post', id],
    () => fetchPost(id),
    {
      staleTime: 10 * 60 * 1000 // 10分钟
    }
  )
}

async function fetchPost(id: string) {
  const response = await fetch(`/api/posts/${id}`)
  if (!response.ok) throw new Error('Failed to fetch post')
  return response.json()
}

// 创建文章Mutation Hook
export function useCreatePost() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (postData: {
      title: string
      content: string
      category?: string
    }) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(postData)
      })

      if (!response.ok) throw new Error('Failed to create post')
      return response.json()
    },
    onSuccess: (newPost) => {
      // 使文章列表缓存失效
      queryClient.invalidateQueries({ queryKey: ['posts'] })

      // 将新文章添加到缓存
      queryClient.setQueryData(['post', newPost.id], newPost)
    },
    onError: (error) => {
      console.error('Error creating post:', error)
    }
  })
}

// 更新文章Mutation Hook
export function useUpdatePost() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({ id, ...updates }: {
      id: string
      title?: string
      content?: string
      category?: string
    }) => {
      const response = await fetch(`/api/posts/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updates)
      })

      if (!response.ok) throw new Error('Failed to update post')
      return response.json()
    },
    onSuccess: (updatedPost) => {
      // 更新缓存中的文章
      queryClient.setQueryData(['post', updatedPost.id], updatedPost)

      // 使文章列表缓存失效
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
    onError: (error) => {
      console.error('Error updating post:', error)
    }
  })
}

表单状态管理

React Hook Form 集成

// src/hooks/useFormState.ts
'use client'

import { useForm, UseFormReturn } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { useState } from 'react'

// 通用表单状态Hook
export function useFormState<T extends z.ZodSchema>(
  schema: T,
  defaultValues?: Partial<z.infer<T>>
) {
  const form = useForm<z.infer<T>>({
    resolver: zodResolver(schema),
    defaultValues,
    mode: 'onChange'
  })

  const [isSubmitting, setIsSubmitting] = useState(false)
  const [submitError, setSubmitError] = useState<string | null>(null)

  const handleSubmit = async (onSubmit: (data: z.infer<T>) => Promise<void>) => {
    const isValid = await form.trigger()

    if (!isValid) {
      return
    }

    setIsSubmitting(true)
    setSubmitError(null)

    try {
      const data = form.getValues()
      await onSubmit(data)
    } catch (error) {
      setSubmitError(error instanceof Error ? error.message : '提交失败')
    } finally {
      setIsSubmitting(false)
    }
  }

  return {
    form,
    isSubmitting,
    submitError,
    handleSubmit,
    clearError: () => setSubmitError(null)
  }
}

// 登录表单Hook
const loginSchema = z.object({
  email: z.string().email('请输入有效的邮箱地址'),
  password: z.string().min(6, '密码至少需要6个字符'),
  remember: z.boolean().default(false)
})

export function useLoginForm() {
  const { form, isSubmitting, submitError, handleSubmit } = useFormState(
    loginSchema,
    {
      email: '',
      password: '',
      remember: false
    }
  )

  return {
    form,
    isSubmitting,
    submitError,
    handleSubmit
  }
}

// 文章编辑表单Hook
const postSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(200, '标题不能超过200个字符'),
  content: z.string().min(10, '内容至少需要10个字符'),
  excerpt: z.string().max(500, '摘要不能超过500个字符').optional(),
  category: z.string().min(1, '请选择分类'),
  tags: z.array(z.string()).default([]),
  published: z.boolean().default(false)
})

export function usePostForm(initialData?: Partial<z.infer<typeof postSchema>>) {
  const { form, isSubmitting, submitError, handleSubmit } = useFormState(
    postSchema,
    initialData
  )

  return {
    form,
    isSubmitting,
    submitError,
    handleSubmit
  }
}

状态持久化

本地存储同步

// src/hooks/useLocalStorage.ts
'use client'

import { useState, useEffect } from 'react'

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // 获取初始值
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') {
      return initialValue
    }

    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error)
      return initialValue
    }
  })

  // 设置值的函数
  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)

      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore))
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error)
    }
  }

  // 监听其他标签页的变化
  useEffect(() => {
    const handleStorageChange = (e: StorageEvent) => {
      if (e.key === key && e.newValue !== null) {
        setStoredValue(JSON.parse(e.newValue))
      }
    }

    if (typeof window !== 'undefined') {
      window.addEventListener('storage', handleStorageChange)
      return () => window.removeEventListener('storage', handleStorageChange)
    }
  }, [key])

  return [storedValue, setValue]
}

// 使用示例
export function useTheme() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>(
    'theme',
    'light'
  )

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }

  return { theme, toggleTheme, setTheme }
}

export function useCart() {
  const [items, setItems] = useLocalStorage<CartItem[]>(
    'cart',
    []
  )

  const addItem = (item: CartItem) => {
    setItems(prev => {
      const existingItem = prev.find(i => i.id === item.id)
      if (existingItem) {
        return prev.map(i =>
          i.id === item.id
            ? { ...i, quantity: i.quantity + item.quantity }
            : i
        )
      }
      return [...prev, item]
    })
  }

  const removeItem = (id: string) => {
    setItems(prev => prev.filter(item => item.id !== id))
  }

  const updateQuantity = (id: string, quantity: number) => {
    if (quantity <= 0) {
      removeItem(id)
    } else {
      setItems(prev =>
        prev.map(item =>
          item.id === id ? { ...item, quantity } : item
        )
      )
    }
  }

  const clearCart = () => {
    setItems([])
  }

  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)

  return {
    items,
    total,
    addItem,
    removeItem,
    updateQuantity,
    clearCart
  }
}

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
  image?: string
}

总结

通过本教程,你已经掌握了:

  1. 本地状态: 组件级状态管理和复杂状态处理
  2. 全局状态: Context API 和 Zustand 的现代状态管理
  3. 服务器状态: React Query 的数据获取和缓存管理
  4. 表单状态: React Hook Form 和 Zod 的表单验证
  5. 状态持久化: 本地存储和状态同步机制
  6. 性能优化: 状态管理的最佳实践和优化策略

下一步学习

  • 深入学习高级状态管理模式(Redux Toolkit, Jotai)
  • 掌握状态管理和调试工具
  • 学习状态测试和验证策略
  • 探索微前端状态管理
  • 了解状态安全性和权限控制

现代 React 应用的状态管理需要根据具体场景选择合适的解决方案。继续探索更多高级特性,构建更强大的应用吧!

文档交叉引用

相关文档

参考章节

  • 📖 [本模块其他章节]: 数据获取基础 | 完整项目实战
  • 📖 [Knowledge Points 快速参考]: 状态管理模式 | React 语法速查

总结

状态选择回顾

先按数据的所有者和失效方式分类,再选工具;不要因为一个库流行就把所有值放进全局 store。

  1. 组件本地状态:只被一个组件树消费的展开、输入和临时 UI 状态,使用 useState;多个状态转移需要统一描述时使用 useReducer,并让 reducer 对相同输入返回可预测结果。
  2. 跨组件客户端状态:主题、当前草稿或未提交的界面偏好可用 Context 或轻量 store。Context 的 value 每次变化会通知消费者,频繁变化的大对象应拆分或选择带 selector 的 store。
  3. 服务器状态:来自 API、会过期且可被其他请求改变的数据,保留请求状态、缓存键、失效条件和重新获取逻辑。不要把一次请求结果复制到全局 store 后就失去刷新与错误语义。
  4. 表单状态:字段值、校验错误和提交状态属于同一次交互。模式校验在提交前给出可读错误,服务端仍必须重新校验;类型推导不能替代运行时输入检查。
  5. 持久化状态:只持久化可以安全恢复且版本兼容的数据。读取 localStorage 只能在客户端发生;为旧数据准备迁移或丢弃策略,绝不保存会话密钥或敏感身份信息。

学习成果验收

  • [ ] 为一个筛选面板标出每个值是本地、跨组件、服务器、表单还是持久化状态,并说明它何时失效。
  • [ ] 用 reducer 实现至少两个状态转移;对非法 action 保持状态不变或显式报错,并用测试或控制台输出验证。
  • [ ] 为一条查询设置稳定缓存键,完成一次编辑后失效并重新获取,确认界面没有继续显示旧列表。
  • [ ] 提交包含错误字段的表单,确认错误贴近字段、请求没有发出;再模拟服务端拒绝并显示可恢复的提交状态。
  • [ ] 刷新页面验证允许持久化的偏好能恢复;删除或损坏存储项后,应用回到安全默认值且不崩溃。

贡献与反馈

内容改进

如果你发现本文档有改进空间,欢迎: – 🐛 报告问题: 在 Issues 中提出具体问题 – 💡 建议改进: 提出修改建议和补充内容 – 📝 参与贡献: 提交 PR 完善文档内容

学习反馈

分享你的学习体验: – ✅ 有用内容: 哪些部分对你最有帮助 – ❓ 疑问点: 哪些内容需要进一步澄清 – 🎯 建议: 希望增加什么内容


文档状态: ✅ 已完成最后更新: 2026年9月版本: v1.0.0

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇