第 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
}
注意点:
value用useMemo,避免每次渲染都新对象导致消费者全重渲染- 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
欢迎交流与反馈