React: 复杂状态:useReducer

最后更新:2026-08-26

1. 你将学到



2. 一个购物车状态管理的故事

(1) 痛点:多个 useState 交互复杂

Alice 在做一个购物车组件,需要管理多个交互:

JSX
function ShoppingCart() {
  const [items, setItems] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [coupon, setCoupon] = useState(null)

  // 添加商品
  function addItem(product) {
    const existing = items.find(i => i.id === product.id)
    if (existing) {
      // ❌ 多个 setState 调用顺序依赖当前值
      setItems(prev => prev.map(i =>
        i.id === product.id ? { ...i, qty: i.qty + 1 } : i
      ))
    } else {
      setItems(prev => [...prev, { ...product, qty: 1 }])
    }
  }

  // 应用优惠券
  function applyCoupon(code) {
    setIsLoading(true)
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          setCoupon(data)
          setError(null)
        } else {
          setError('优惠券无效')
        }
      })
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false))
  }

  // 问题:items、isLoading、error、coupon 四个状态相互关联
  // 一个操作(如 applyCoupon)要操作 3 个 setState
  // 容易漏掉某个状态的更新,导致状态不一致
}
▶ 试一试

(2) useReducer 的解法

JSX
// 1. 定义初始状态
const initialState = {
  items: [],
  isLoading: false,
  error: null,
  coupon: null,
  total: 0
}

// 2. 定义 reducer(纯函数:接收旧状态 + action → 返回新状态)
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find(i => i.id === action.product.id)
      const newItems = existing
        ? state.items.map(i =>
            i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i
          )
        : [...state.items, { ...action.product, qty: 1 }]
      return { ...state, items: newItems, error: null }
    }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id)
      }
    case 'UPDATE_QTY':
      return {
        ...state,
        items: state.items.map(i =>
          i.id === action.id ? { ...i, qty: Math.max(1, action.qty) } : i
        )
      }
    case 'APPLY_COUPON_REQUEST':
      return { ...state, isLoading: true, error: null }
    case 'APPLY_COUPON_SUCCESS':
      return { ...state, isLoading: false, coupon: action.coupon }
    case 'APPLY_COUPON_FAILURE':
      return { ...state, isLoading: false, error: action.error }
    case 'CLEAR_ERROR':
      return { ...state, error: null }
    case 'RESET_CART':
      return initialState
    default:
      return state
  }
}

// 3. 在组件中使用
function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState)

  function handleAdd(product) {
    dispatch({ type: 'ADD_ITEM', product })
  }

  function handleApplyCoupon(code) {
    dispatch({ type: 'APPLY_COUPON_REQUEST' })
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          dispatch({ type: 'APPLY_COUPON_SUCCESS', coupon: data })
        } else {
          dispatch({ type: 'APPLY_COUPON_FAILURE', error: '优惠券无效' })
        }
      })
      .catch(err => dispatch({ type: 'APPLY_COUPON_FAILURE', error: err.message }))
  }

  return (
    <div>
      {state.items.map(item => (
        <div key={item.id}>
          {item.name} × {item.qty}
          <button onClick={() => dispatch({ type: 'UPDATE_QTY', id: item.id, qty: item.qty + 1 })}>+</button>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>删除</button>
        </div>
      ))}
      {state.isLoading && <p>处理中...</p>}
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.coupon && <p>已使用优惠券:{state.coupon.code}</p>}
    </div>
  )
}

收益:所有状态变更集中在一个 reducer 函数中——可预测、可测试、容易调试。每个 action 就是一个"发生了什么"的记录。



3. useReducer 的核心模式

概念 说明 示例
reducer 纯函数,(state, action) → newState function cartReducer(state, action)
action 描述"发生了什么"的对象 { type: 'ADD_ITEM', payload: product }
dispatch 发送 action 的函数 dispatch({ type: 'INCREMENT' })
initialState 初始状态对象 { items: [], total: 0 }
init 函数 惰性初始化(可选第 3 参数) useReducer(reducer, initialArg, init)
100%
graph LR
    A[组件] -->|dispatch(action)| B[reducer]
    B -->|(state, action) => newState| C[新状态]
    C -->|更新| A
    D[用户操作] -->|触发| A
    
    style B fill:#1890ff,color:#fff
    style C fill:#52c41a,color:#fff

(1) 基本语法

JSX
const [state, dispatch] = useReducer(reducer, initialState)

// reducer 是一个纯函数:
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'SET_NAME':
      return { ...state, name: action.payload }
    default:
      return state
  }
}
▶ 试一试

(2) Action 设计原则

原则 说明 坏例子 好例子
类型命名用过去式 描述"已经发生了什么" ADD ITEM_ADDED
携带最少数据 只带必要数据,其他在 reducer 中计算 dispatch({ type: 'SET_TOTAL', total: 42 }) dispatch({ type: 'ITEM_ADDED', item }),total 在 reducer 中计算
不要处理副作用 reducer 必须是纯函数(不调 API、不读写 localStorage) case 'LOGIN': fetch(...) 在 useEffect 或事件处理中调 API,完成后 dispatch


4. useReducer vs useState

维度 useState useReducer
状态量 3 个以下 3 个以上或状态相互关联
更新逻辑 简单(toggle、+1) 复杂(多个子值相互影响)
可读性 好(一眼看懂) 好(集中管理)
可测试性 需渲染组件测试 reducer 是纯函数,可直接测试
适用规模 小组件、独立状态 中大型组件、复杂表单、购物车

▶ 示例:同功能不同实现对比

JSX 📖 仅展示
// ============================================
// 示例:计数器——useState vs useReducer
// ============================================

// ---- useState 版(简单场景)----
function CounterWithState() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  )
}

// ---- useReducer 版(复杂场景)----
const initialState = { count: 0, lastAction: null, history: [] }

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
        lastAction: 'INCREMENT',
        history: [...state.history, 'INCREMENT']
      }
    case 'DECREMENT':
      return {
        count: state.count - 1,
        lastAction: 'DECREMENT',
        history: [...state.history, 'DECREMENT']
      }
    case 'RESET':
      return initialState
    case 'SET':
      return { ...state, count: action.value }
    default:
      return state
  }
}

function CounterWithReducer() {
  const [state, dispatch] = useReducer(counterReducer, initialState)
  return (
    <div>
      <p>计数:{state.count}</p>
      <p>上次操作:{state.lastAction}</p>
      <p>操作次数:{state.history.length}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
      <button onClick={() => dispatch({ type: 'SET', value: 100 })}>设为 100</button>
    </div>
  )
}
逻辑代码 47 行(超过 40 行限制,仅展示)

5. useReducer + Context:轻量全局状态

当多个组件需要共享同一个 reducer 状态时,用 Context 传递 dispatch:

JSX
// ============================================
// 示例:Todo 应用(useReducer + Context)
// 功能:全局状态管理,任何子组件都可以 dispatch
// ============================================

// ---- 1. 创建 Context ----
const TodoContext = React.createContext(null)

// ---- 2. 定义 reducer ----
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }]
    case 'TOGGLE_TODO':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      )
    case 'DELETE_TODO':
      return state.filter(t => t.id !== action.id)
    case 'CLEAR_DONE':
      return state.filter(t => !t.done)
    default:
      return state
  }
}

// ---- 3. Provider 包裹 ----
function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, [
    { id: 1, text: '学习 useReducer', done: true },
    { id: 2, text: '理解 Context 用法', done: false }
  ])

  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

// ---- 4. 任意子组件读取/修改状态 ----
function AddTodo() {
  const [text, setText] = useState('')
  const { dispatch } = useContext(TodoContext)

  function handleSubmit(e) {
    e.preventDefault()
    if (!text.trim()) return
    dispatch({ type: 'ADD_TODO', text: text.trim() })
    setText('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder="添加待办..." />
      <button type="submit">添加</button>
    </form>
  )
}

function TodoList() {
  const { todos, dispatch } = useContext(TodoContext)

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          <input type="checkbox" checked={todo.done}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })} />
          {todo.text}
          <button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>✕</button>
        </li>
      ))}
    </ul>
  )
}

function TodoStats() {
  const { todos } = useContext(TodoContext)
  const done = todos.filter(t => t.done).length
  const pending = todos.length - done

  return (
    <p style={{ fontSize: '13px', color: '#666' }}>
      总计 {todos.length} | 已完成 {done} | 待完成 {pending}
      {done > 0 && (
        <button onClick={() => dispatch({ type: 'CLEAR_DONE' })} style={{ marginLeft: '8px' }}>
          清除已完成
        </button>
      )}
    </p>
  )
}

// ---- 5. 顶层使用 ----
function App() {
  return (
    <TodoProvider>
      <div style={{ maxWidth: '500px', margin: '0 auto' }}>
        <h2>📋 Todo(useReducer + Context)</h2>
        <AddTodo />
        <TodoStats />
        <TodoList />
      </div>
    </TodoProvider>
  )
}


6. 完整示例:多步骤表单(Wizard)

JSX
// ============================================
// 完整示例:多步骤注册表单(Wizard)
// 功能:useReducer 管理多步骤表单的复杂状态
// ============================================

const wizardInitialState = {
  currentStep: 1,
  formData: {
    account: { username: '', email: '', password: '' },
    profile: { name: '', bio: '', avatar: '' },
    preferences: { language: 'zh', theme: 'light', newsletter: false }
  },
  errors: {},
  isSubmitting: false,
  completed: false
}

function wizardReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      if (state.currentStep >= 3) return state
      return { ...state, currentStep: state.currentStep + 1, errors: {} }

    case 'PREV_STEP':
      if (state.currentStep <= 1) return state
      return { ...state, currentStep: state.currentStep - 1 }

    case 'UPDATE_FIELD':
      return {
        ...state,
        formData: {
          ...state.formData,
          [action.section]: {
            ...state.formData[action.section],
            [action.field]: action.value
          }
        }
      }

    case 'SET_ERRORS':
      return { ...state, errors: action.errors }

    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, errors: {} }

    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, completed: true }

    case 'SUBMIT_FAILURE':
      return { ...state, isSubmitting: false, errors: { submit: action.error } }

    case 'RESET':
      return wizardInitialState

    default:
      return state
  }
}

function WizardForm() {
  const [state, dispatch] = useReducer(wizardReducer, wizardInitialState)

  function validateStep() {
    const errors = {}
    const data = state.formData[getStepKey(state.currentStep)]
    
    if (state.currentStep === 1) {
      if (!data.username) errors.username = '必填'
      if (!data.email?.includes('@')) errors.email = '无效邮箱'
      if (!data.password || data.password.length < 6) errors.password = '密码至少 6 位'
    }
    if (state.currentStep === 2) {
      if (!data.name) errors.name = '必填'
    }
    
    return errors
  }

  function handleNext() {
    const errors = validateStep()
    if (Object.keys(errors).length > 0) {
      dispatch({ type: 'SET_ERRORS', errors })
      return
    }
    dispatch({ type: 'NEXT_STEP' })
  }

  function handleSubmit() {
    dispatch({ type: 'SUBMIT_START' })
    // 模拟 API 请求
    setTimeout(() => {
      dispatch({ type: 'SUBMIT_SUCCESS' })
    }, 1500)
  }

  if (state.completed) {
    return (
      <div style={{ textAlign: 'center', padding: '40px' }}>
        <h2>🎉 注册成功!</h2>
        <pre style={{ textAlign: 'left', background: '#f5f5f5', padding: '16px', borderRadius: '4px' }}>
          {JSON.stringify(state.formData, null, 2)}
        </pre>
        <button onClick={() => dispatch({ type: 'RESET' })}>重新注册</button>
      </div>
    )
  }

  const stepKey = getStepKey(state.currentStep)

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📝 注册(步骤 {state.currentStep}/3)</h2>

      {/* 步骤指示器 */}
      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
        {['账号信息', '个人资料', '偏好设置'].map((label, i) => (
          <div key={i} style={{
            flex: 1, padding: '8px', textAlign: 'center',
            backgroundColor: state.currentStep >= i + 1 ? '#1890ff' : '#f0f0f0',
            color: state.currentStep >= i + 1 ? 'white' : '#999',
            borderRadius: '4px', fontSize: '13px'
          }}>
            {label}
          </div>
        ))}
      </div>

      {/* 表单内容 */}
      {state.currentStep === 1 && <AccountForm formData={state.formData.account} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 2 && <ProfileForm formData={state.formData.profile} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 3 && <PreferenceForm formData={state.formData.preferences} errors={state.errors} dispatch={dispatch} />}

      {/* 错误提示 */}
      {state.errors.submit && <p style={{ color: '#ff4d4f' }}>{state.errors.submit}</p>}

      {/* 操作按钮 */}
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
        <button onClick={() => dispatch({ type: 'PREV_STEP' })} disabled={state.currentStep === 1}
          style={btnStyle(state.currentStep === 1 ? '#d9d9d9' : '#666')}>
          上一步
        </button>
        {state.currentStep < 3 ? (
          <button onClick={handleNext} style={btnStyle('#1890ff')}>
            下一步
          </button>
        ) : (
          <button onClick={handleSubmit} disabled={state.isSubmitting} style={btnStyle(state.isSubmitting ? '#d9d9d9' : '#52c41a')}>
            {state.isSubmitting ? '提交中...' : '提交注册'}
          </button>
        )}
      </div>
    </div>
  )
}

function getStepKey(step) {
  return ['account', 'profile', 'preferences'][step - 1]
}

function AccountForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="username" value={formData.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'username', value: e.target.value })}
        placeholder="用户名" style={inputStyle(errors.username)} />
      {errors.username && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.username}</p>}
      <input name="email" value={formData.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'email', value: e.target.value })}
        placeholder="邮箱" style={inputStyle(errors.email)} />
      {errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
      <input name="password" type="password" value={formData.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'password', value: e.target.value })}
        placeholder="密码" style={inputStyle(errors.password)} />
      {errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
    </div>
  )
}

function ProfileForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="name" value={formData.name} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'name', value: e.target.value })}
        placeholder="姓名" style={inputStyle(errors.name)} />
      {errors.name && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.name}</p>}
      <textarea name="bio" value={formData.bio} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'bio', value: e.target.value })}
        placeholder="个人简介" rows={3} style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px', marginBottom: '8px' }} />
    </div>
  )
}

function PreferenceForm({ formData, errors, dispatch }) {
  return (
    <div>
      <p>界面语言:</p>
      {['zh', 'en', 'ja', 'pt', 'ar'].map(lang => (
        <label key={lang} style={{ marginRight: '12px' }}>
          <input type="radio" checked={formData.language === lang}
            onChange={() => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'language', value: lang })} />
          {lang.toUpperCase()}
        </label>
      ))}
      <p style={{ marginTop: '16px' }}>主题:</p>
      <select value={formData.theme} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'theme', value: e.target.value })}
        style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <label style={{ display: 'block', marginTop: '12px' }}>
        <input type="checkbox" checked={formData.newsletter}
          onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'newsletter', value: e.target.checked })} />
        订阅技术周报
      </label>
    </div>
  )
}

function inputStyle(hasError) {
  return {
    width: '100%', padding: '8px', marginBottom: '4px',
    border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
    borderRadius: '4px'
  }
}

function btnStyle(bgColor) {
  return {
    padding: '10px 24px', backgroundColor: bgColor,
    color: 'white', border: 'none', borderRadius: '4px',
    cursor: bgColor === '#d9d9d9' ? 'not-allowed' : 'pointer'
  }
}

预期输出:3 步注册表单(账号 → 个人资料 → 偏好设置),每步有校验,可前进后退,最终提交并显示提交结果。


▶ 示例 2:异步数据加载与 useReducer

JSX 📖 仅展示
// ============================================
// 示例:异步数据加载——useReducer 管理请求三态
// 功能:展示 useReducer 如何优雅地处理 loading / success / error 三种状态
// ============================================

import { useReducer, useEffect } from 'react'

// 1. 定义状态和 action 类型
const initialState = {
  data: null,
  loading: true,
  error: null
}

function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { data: action.payload, loading: false, error: null }
    case 'FETCH_ERROR':
      return { data: null, loading: false, error: action.error }
    case 'RESET':
      return initialState
    default:
      return state
  }
}

// 2. 组件中使用
function UserDataFetcher({ userId }) {
  const [state, dispatch] = useReducer(dataReducer, initialState)

  useEffect(() => {
    if (!userId) return

    dispatch({ type: 'FETCH_START' })

    // 模拟 API 请求
    const timer = setTimeout(() => {
      if (userId <= 0) {
        dispatch({ type: 'FETCH_ERROR', error: '无效的用户 ID' })
      } else {
        // 模拟成功响应
        const mockUser = {
          id: userId,
          name: `用户 ${userId}`,
          email: `user${userId}@example.com`,
          role: userId === 1 ? '管理员' : '普通用户',
          joinDate: '2026-01-15'
        }
        dispatch({ type: 'FETCH_SUCCESS', payload: mockUser })
      }
    }, 1000)

    return () => {
      clearTimeout(timer)
      dispatch({ type: 'RESET' })
    }
  }, [userId])

  // 3. 根据状态渲染不同 UI
  if (state.loading) {
    return (
      <div style={{ textAlign: 'center', padding: '20px' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>⏳</div>
        <p style={{ color: '#666' }}>正在加载用户数据...</p>
      </div>
    )
  }

  if (state.error) {
    return (
      <div style={{ textAlign: 'center', padding: '20px', color: '#ff4d4f' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>❌</div>
        <p>加载失败:{state.error}</p>
      </div>
    )
  }

  if (!state.data) {
    return <p style={{ color: '#999', textAlign: 'center' }}>请选择一个用户</p>
  }

  const { name, email, role, joinDate } = state.data
  return (
    <div style={{
      border: '1px solid #e8e8e8',
      borderRadius: '8px',
      padding: '20px',
      maxWidth: '400px',
      margin: '0 auto'
    }}>
      <h3>👤 用户信息</h3>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <tbody>
          {[
            ['姓名', name],
            ['邮箱', email],
            ['角色', role],
            ['注册日期', joinDate]
          ].map(([label, value]) => (
            <tr key={label}>
              <td style={{ padding: '8px', color: '#666', fontWeight: 'bold', width: '80px' }}>{label}</td>
              <td style={{ padding: '8px' }}>{value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

// 使用
function App() {
  const [userId, setUserId] = useState(1)
  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📡 异步数据加载示例</h2>
      <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
        {[1, 2, 3].map(id => (
          <button key={id} onClick={() => setUserId(id)}
            style={{
              padding: '6px 16px',
              backgroundColor: userId === id ? '#1890ff' : '#f0f0f0',
              color: userId === id ? 'white' : '#333',
              border: 'none', borderRadius: '4px', cursor: 'pointer'
            }}>
            用户 {id}
          </button>
        ))}
        <button onClick={() => setUserId(-1)}
          style={{ padding: '6px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          触发错误
        </button>
      </div>
      <UserDataFetcher userId={userId} />
    </div>
  )
}
逻辑代码 117 行(超过 40 行限制,仅展示)

▶ 示例 3:useReducer 实现 Undo/Redo 功能

JSX
function undoReducer(state, action) {
  switch (action.type) {
    case 'SET':
      return { past: [...state.past, state.present], present: action.payload, future: [] }
    case 'UNDO':
      if (state.past.length === 0) return state
      const previous = state.past[state.past.length - 1]
      return { past: state.past.slice(0, -1), present: previous, future: [state.present, ...state.future] }
    case 'REDO':
      if (state.future.length === 0) return state
      const next = state.future[0]
      return { past: [...state.past, state.present], present: next, future: state.future.slice(1) }
    default:
      return state
  }
}

function UndoApp() {
  const [{ past, present, future }, dispatch] = useReducer(undoReducer, {
    past: [], present: '', future: [],
  })
  const [input, setInput] = useState('')

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Undo/Redo Editor</h3>
      <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type something..."
        style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <button onClick={() => { dispatch({ type: 'SET', payload: input }); setInput('') }}
          style={{ padding: '6px 16px', cursor: 'pointer' }}>Save</button>
        <button onClick={() => dispatch({ type: 'UNDO' })} disabled={past.length === 0}
          style={{ padding: '6px 16px', cursor: past.length ? 'pointer' : 'not-allowed' }}>Undo</button>
        <button onClick={() => dispatch({ type: 'REDO' })} disabled={future.length === 0}
          style={{ padding: '6px 16px', cursor: future.length ? 'pointer' : 'not-allowed' }}>Redo</button>
      </div>
      <p>Current: <strong>{present || '(empty)'}</strong></p>
      <p style={{ fontSize: 12, color: '#999' }}>Past: [{past.join(', ')}] | Future: [{future.join(', ')}]</p>
    </div>
  )
}
▶ 试一试

▶ 示例 4:useReducer 管理表单状态

JSX 📖 仅展示
const initialForm = { username: '', email: '', password: '', role: 'user', agree: false, errors: {} }

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value, errors: { ...state.errors, [action.field]: undefined } }
    case 'SET_ERRORS':
      return { ...state, errors: action.errors }
    case 'RESET':
      return initialForm
    default:
      return state
  }
}

function FormWithReducer() {
  const [form, dispatch] = useReducer(formReducer, initialForm)

  function validate() {
    const errors = {}
    if (!form.username.trim()) errors.username = 'Required'
    if (!form.email.includes('@')) errors.email = 'Invalid email'
    if (form.password.length < 6) errors.password = 'At least 6 chars'
    if (!form.agree) errors.agree = 'Must agree'
    return errors
  }

  function handleSubmit(e) {
    e.preventDefault()
    const errors = validate()
    if (Object.keys(errors).length > 0) { dispatch({ type: 'SET_ERRORS', errors }); return }
    alert(`Submitted: ${form.username}`)
  }

  function fieldStyle(key) {
    return { width: '100%', padding: 8, borderRadius: 4, border: form.errors[key] ? '1px solid #ff4d4f' : '1px solid #d9d9d9', marginBottom: 4 }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Registration</h3>
      <input value={form.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'username', value: e.target.value })} placeholder="Username" style={fieldStyle('username')} />
      {form.errors.username && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.username}</p>}
      <input value={form.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'email', value: e.target.value })} placeholder="Email" style={fieldStyle('email')} />
      {form.errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.email}</p>}
      <input type="password" value={form.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'password', value: e.target.value })} placeholder="Password" style={fieldStyle('password')} />
      {form.errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.password}</p>}
      <label style={{ display: 'block', margin: '8px 0' }}>
        <input type="checkbox" checked={form.agree} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'agree', value: e.target.checked })} /> I agree
      </label>
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
      <button type="button" onClick={() => dispatch({ type: 'RESET' })} style={{ padding: '8px 16px', marginLeft: 8, cursor: 'pointer' }}>Reset</button>
    </form>
  )
}
逻辑代码 49 行(超过 40 行限制,仅展示)

▶ 示例 5:useReducer + Context 全局购物车

JSX 📖 仅展示
const CartContext = createContext(null)

const cartInitial = { items: [], coupon: null }

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD': {
      const existing = state.items.find(i => i.id === action.product.id)
      return {
        ...state,
        items: existing
          ? state.items.map(i => i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i)
          : [...state.items, { ...action.product, qty: 1 }],
      }
    }
    case 'REMOVE':
      return { ...state, items: state.items.filter(i => i.id !== action.id) }
    case 'SET_QTY':
      return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) }
    case 'CLEAR':
      return cartInitial
    default:
      return state
  }
}

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, cartInitial)
  const total = useMemo(() => state.items.reduce((sum, i) => sum + i.price * i.qty, 0), [state.items])
  const value = useMemo(() => ({ ...state, total, dispatch }), [state, total])
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>
}

function useCart() { return useContext(CartContext) }

function ProductList() {
  const { dispatch } = useCart()
  const products = [{ id: 1, name: 'Keyboard', price: 79 }, { id: 2, name: 'Mouse', price: 49 }, { id: 3, name: 'Monitor', price: 399 }]
  return (
    <div>
      <h3>Products</h3>
      {products.map(p => (
        <div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 8, borderBottom: '1px solid #f0f0f0' }}>
          <span>{p.name} - ${p.price}</span>
          <button onClick={() => dispatch({ type: 'ADD', product: p })} style={{ cursor: 'pointer' }}>Add</button>
        </div>
      ))}
    </div>
  )
}

function CartSummary() {
  const { items, total, dispatch } = useCart()
  return (
    <div>
      <h3>Cart ({items.length} items)</h3>
      {items.map(i => (
        <div key={i.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 4 }}>
          <span>{i.name} x{i.qty}</span>
          <span>${i.price * i.qty}</span>
        </div>
      ))}
      <hr />
      <strong>Total: ${total}</strong>
      <button onClick={() => dispatch({ type: 'CLEAR' })} style={{ marginLeft: 8, cursor: 'pointer' }}>Clear</button>
    </div>
  )
}
逻辑代码 62 行(超过 40 行限制,仅展示)

❓ 常见问题

Q 为什么 reducer 必须是纯函数?
A 纯函数意味着:相同输入 → 相同输出;不修改外部变量;不产生副作用(不调 API、不读写文件)。React 依赖 reducer 的可预测性来做性能优化(跳过不必要的渲染)和调试(时间旅行)。在 reducer 里调 API 会导致状态变化不可预测,破坏调试体验。
Q 什么时候应该从 useState 切换到 useReducer?
A 当出现以下 3 种情况之一时:① 一个操作需要更新 3 个以上 useState(如点击按钮同时修改 isLoading、error、data);② 状态逻辑复杂到分散在多个事件处理函数中难以追踪;③ 组件有多个相互依赖的状态子值。切换的时机:当 useState 版本让你的组件超过 100 行时。
Q reducer 里面可以写副作用吗(如 fetch 请求)?
A 绝对不行。reducer 必须是纯函数——同样的输入永远返回同样的输出,不能有 API 调用、DOM 操作、随机数等副作用。副作用应该放在 useEffect 或事件处理函数中。reducer 中写副作用会导致:状态不可预测、调试困难、时间旅行(DevTools 回放)失效。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 Counter 组件,用 useReducer 实现 +1、-1、+10、重置四个操作。在页面上显示当前值和操作历史(最近 5 次)。
  2. 进阶题(难度⭐⭐):创建一个 TodoApp 组件,用 useReducer + Context 实现添加、切换完成、编辑、删除、清除已完成五个功能。
  3. 挑战题(难度⭐⭐⭐):创建一个 UndoRedoList 组件,用 useReducer 实现"撤销/重做"功能。每个操作保存历史快照,支持 Ctrl+Z 撤销、Ctrl+Shift+Z 重做。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏