mCell
7 / 10

useReducer

从“复杂状态用 reducer 管理”到 action 设计、不可变更新、与 useState/useContext 的组合实践

先用一句话理解 useReducer

useReducer 用来以“reducer + action”的方式管理状态:你派发 action,reducer 根据旧状态计算新状态,让复杂更新逻辑更集中、更可预测。


为什么会需要 useReducer

useState 很适合简单状态,但当你遇到这些信号时,useReducer 往往更舒服:

  • 状态字段多、更新分散,靠 setXxx 很难读
  • 多个字段要一起变(一次交互触发多处变化)
  • 更新规则复杂,需要更强的可维护性/可测试性
  • 你想把“如何更新”从组件逻辑里抽出去

一句话:状态越像“状态机”,越适合 reducer。


最基本用法

jsx
import { useReducer } from 'react' function reducer(state, action) { switch (action.type) { case 'inc': return { ...state, count: state.count + 1 } case 'dec': return { ...state, count: state.count - 1 } default: return state } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }) return ( <> <div>{state.count}</div> <button onClick={() => dispatch({ type: 'dec' })}>-</button> <button onClick={() => dispatch({ type: 'inc' })}>+</button> </> ) }

你会得到两个东西:

  • state:当前状态
  • dispatch(action):派发动作,触发 reducer 计算新状态并重新渲染

reducer 是什么

reducer 是一个纯函数:

js
nextState = reducer(prevState, action)

要求(强烈建议):

  • 不做副作用(不发请求、不操作 DOM、不读写本地存储)
  • 不修改入参(不要直接改 prevState)
  • 同样输入得到同样输出(可预测)

action 怎么设计更好用

最简单:字符串 type

js
dispatch({ type: 'toggle' })

带 payload

js
dispatch({ type: 'setName', payload: 'Alice' })

多字段 payload

js
dispatch({ type: 'updateForm', payload: { name: 'A', age: 18 } })

经验法则:

  • type 用动词:add/remove/toggle/set/update
  • payload 只携带 reducer 需要的最小信息

不可变更新:必须返回新对象/新数组

错误:原地修改

js
state.items.push(action.payload) return state

正确:创建新引用

js
return { ...state, items: [...state.items, action.payload] }

因为 React 判断是否需要更新依赖“引用变化”,原地改会导致很多问题(包括 memo、effect、调试难度)。


useReducer 与 useState 的关键差异

  • useState:你直接给新值 / 或给更新函数
  • useReducer:你统一通过 dispatch(action) 来表达“发生了什么”,由 reducer 决定“状态如何变”

对应关系可以这样理解:

  • setCount(c => c + 1) 约等于 dispatch({ type: "inc" })
  • 但 reducer 可以把一堆相关更新集中起来处理

惰性初始化:init 函数

当初始 state 计算成本高,或者需要从 props 推导初始值:

jsx
function init(initialCount) { return { count: initialCount, history: [] } } const [state, dispatch] = useReducer(reducer, 10, init)

第三个参数 init 只会在首次挂载时调用一次。


把副作用放哪里

reducer 应该纯,那么请求/订阅这种副作用放哪里?

常见模式:

  • 在事件处理里 dispatch(同步)
  • useEffect 监听 state 或某些 action 结果来做副作用(异步)

示例:提交表单后发请求

jsx
function reducer(state, action) { switch (action.type) { case 'submit': return { ...state, submitting: true, error: null } case 'success': return { ...state, submitting: false, data: action.payload } case 'error': return { ...state, submitting: false, error: action.payload } default: return state } } function Form() { const [state, dispatch] = useReducer(reducer, { submitting: false, data: null, error: null, }) const onSubmit = async () => { dispatch({ type: 'submit' }) try { const res = await fetch('/api', { method: 'POST' }) dispatch({ type: 'success', payload: await res.json() }) } catch (e) { dispatch({ type: 'error', payload: String(e) }) } } return ( <button onClick={onSubmit} disabled={state.submitting}> {state.submitting ? 'submitting...' : 'submit'} </button> ) }

与 useContext 组合:轻量“全局状态”

典型写法:Context 提供 {state, dispatch}

jsx
import { createContext, useContext, useMemo, useReducer } from 'react' const StoreContext = createContext(null) function reducer(state, action) { switch (action.type) { case 'add': return { ...state, todos: [...state.todos, action.text] } default: return state } } export function StoreProvider({ children }) { const [state, dispatch] = useReducer(reducer, { todos: [] }) const value = useMemo(() => ({ state, dispatch }), [state]) return <StoreContext.Provider value={value}>{children}</StoreContext.Provider> } export function useStore() { const ctx = useContext(StoreContext) if (!ctx) throw new Error('useStore must be used within StoreProvider') return ctx }

注意点:

  • valueuseMemo,避免每次渲染都新对象导致消费者全重渲染
  • state 变化仍会让所有 useContext 的组件更新;想更细粒度需要拆 Context 或用带 selector 的方案

常见误区清单

  • reducer 里做副作用(请求/订阅/随机数/读写 storage)
  • 原地修改 state(push/splice/直接赋值)
  • action 设计随意导致 reducer 变成“巨石函数”难维护
  • 把所有全局状态都塞进一个 context,任何变化都导致全体重渲染
  • 以为 useReducer 一定更快(它更偏向结构化与可维护性)

记忆模型

  • dispatch 表达“发生了什么”
  • reducer 决定“状态怎么变”
  • reducer 要纯、更新要不可变
  • 复杂状态用 reducer,让逻辑集中、可预测、易测试

最小练习题

实现一个 Todo reducer,支持:

  • 添加:{ type: "add", text }
  • 切换完成:{ type: "toggle", id }
  • 删除:{ type: "remove", id }

数据结构:

js
{ todos: [{ id, text, done }] }

参考 reducer:

jsx
function reducer(state, action) { switch (action.type) { case 'add': return { ...state, todos: [ ...state.todos, { id: Date.now(), text: action.text, done: false }, ], } case 'toggle': return { ...state, todos: state.todos.map((t) => t.id === action.id ? { ...t, done: !t.done } : t, ), } case 'remove': return { ...state, todos: state.todos.filter((t) => t.id !== action.id), } default: return state } }

留言讨论

Discussion

欢迎交流与反馈