
一句话:
useReducer把「状态」与「如何更新状态」的规则拆开——状态在 Hook 里,更新规则写在纯函数 reducer 里;它适合「状态之间有关联、转移逻辑复杂、需要集中管理与测试」的场景。
一、签名与基本用法
const [state, dispatch] = useReducer(reducer, initialState);
const [state2, dispatch2] = useReducer(reducer, initialArg, init); // 惰性初始化
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
default:
return state; // ⭐ 必须返回 state(不能返回 undefined)
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
{state.count}
>
);
}
type State = { count: number };
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: action.payload };
default: return state;
}
}
// 惰性初始化(第三个参数)
function init(initialCount: number): State {
return { count: computeExpensiveInitial(initialCount) };
}
const [state, dispatch] = useReducer(reducer, 0, init);
// ⭐ init 只在首次渲染调用
二、与 useState 的对比
| 维度 | useState |
useReducer |
|---|---|---|
| 状态粒度 | 一个值(可以是对象) | 一个对象(通常包含多个相关字段) |
| 更新方式 | 直接给新值 / 函数式更新 | dispatch(action) |
| 逻辑位置 | 组件内(分散在各处) | 集中在 reducer 里 |
| 可测试性 | 需要渲染组件才能测 | reducer 是纯函数,可直接单测 |
| 依赖关系 | 多个 state 之间的联动要手动同步 | 状态转移写在一起,天然一致 |
| 代码量 | 简单场景更少 | 复杂场景更少(样板集中) |
| 引用稳定性 | setState 稳定 |
dispatch 稳定 |
| 适合 | 简单独立的值 | 复杂联动状态、状态机、需要撤销/重做 |
// 同一个需求的两写法对比:一个「带校验的表单状态」
// ❌ useState:分散、容易不同步
function FormUseState() {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [submitting, setSubmitting] = useState(false);
const handleChange = (name, value) => {
setValues((v) => ({ ...v, [name]: value }));
setTouched((t) => ({ ...t, [name]: true }));
setErrors((e) => ({ ...e, [name]: validate(name, value) })); // ⚠️ 三处同步
};
// ...
}
// ✅ useReducer:一处集中
function FormUseReducer() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = (name, value) => dispatch({ type: 'change', name, value });
// reducer 内部一次性处理 values / touched / errors / dirty
}
三、什么时候该用 useReducer
✅ 四个信号:
① 多个状态总是一起变化(如 values + errors + touched + dirty)
→ 用对象 + reducer 保证一致性
② 下一个状态依赖「上一个状态的多个字段」
→ reducer 里能拿到完整 state,逻辑自然
③ 状态转移逻辑复杂,需要「集中、可测试」
→ reducer 是纯函数,直接单测
④ 需要「状态机」式的建模(避免非法状态组合)
→ 用判别联合 + reducer 天然表达
❌ 不该用的场景:
· 只是一个独立的简单值(useState 更轻)
· 状态之间毫无关联(拆成多个 useState 更清晰)
· 需要「跨组件共享」(那是状态管理库的活)
四、reducer 必须是纯函数
// ✅ 纯:相同输入 → 相同输出,无副作用,不修改参数
function reducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, list: [...state.list, action.item] }; // 返回新对象
case 'remove':
return { ...state, list: state.list.filter((i) => i.id !== action.id) };
default:
return state;
}
}
// ❌ 不纯的各种写法
function badReducer(state, action) {
state.count++; // ❌ 直接改参数
state.list.push(action.item); // ❌ 原地修改
fetch('/api/log'); // ❌ 副作用
Math.random(); // ❌ 不确定的结果
Date.now(); // ❌ 不确定的结果
return state;
}
| 要求 | 原因 |
|---|---|
| 不修改参数(用不可变更新) | React 靠引用比较判断变化;且 StrictMode 会双调用 |
| 无副作用(不改外部变量、不发请求) | reducer 可能在渲染阶段被调用(含双调用),副作用会重复执行 |
| 结果可预测(不用随机值/时间) | 保证「相同输入得到相同输出」 |
必须有 default 返回原 state |
未识别的 action 不能返回 undefined |
// ⚠️ 副作用应该放在哪里?
// ❌ 不要在 reducer 里
function badReducer(state, action) {
if (action.type === 'submit') {
api.post(state); // ❌
}
return state;
}
// ✅ 在事件处理或 useEffect 里
function Comp() {
const [state, dispatch] = useReducer(reducer, initial);
const handleSubmit = async () => {
dispatch({ type: 'submitStart' });
try {
const res = await api.post(state.values);
dispatch({ type: 'submitSuccess', data: res });
} catch (e) {
dispatch({ type: 'submitError', error: String(e) });
}
};
}
五、dispatch 的稳定性与「读最新 state」
// ① dispatch 的引用在组件生命周期内稳定 → 可以安全放进依赖数组
const [state, dispatch] = useReducer(reducer, initial);
useEffect(() => {
dispatch({ type: 'sync' });
}, []); // ✅ 无需把 dispatch 写进依赖(它稳定)
// ② dispatch 不返回新值,不能像 setState 那样「读旧值」
// ❌ 没有这种写法
const next = dispatch({ type: 'inc' });
// ✅ 需要「基于当前值计算」时,把「计算」也交给 reducer
dispatch({ type: 'add', amount: 1 }); // reducer 里用 state.count + amount
// ③ 需要「读取最新 state」时用 ref
const stateRef = useRef(state);
useEffect(() => { stateRef.current = state; });
const handleAsync = async () => {
await delay(1000);
dispatch({ type: 'set', value: stateRef.current.count + 1 }); // 读最新值
};
六、用 useReducer 做状态机
// ✅ 判别联合 + reducer = 天然的状态机
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
type Action =
| { type: 'fetch' }
| { type: 'success'; data: User[] }
| { type: 'error'; error: string }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'fetch':
return { status: 'loading' };
case 'success':
return { status: 'success', data: action.data };
case 'error':
return { status: 'error', error: action.error };
case 'reset':
return { status: 'idle' };
default:
return state;
}
}
// 使用:状态与 UI 一一对应,不存在非法组合
function UserList() {
const [state, dispatch] = useReducer(reducer, { status: 'idle' });
useEffect(() => {
let cancelled = false;
dispatch({ type: 'fetch' });
api.getUsers()
.then((data) => { if (!cancelled) dispatch({ type: 'success', data }); })
.catch((e) => { if (!cancelled) dispatch({ type: 'error', error: String(e) }); });
return () => { cancelled = true; };
}, []);
switch (state.status) {
case 'idle': return null;
case 'loading': return ;
case 'error': return ;
case 'success': return
; // ⭐ 只有这里能访问 data
}
}
用 reducer 做状态机的好处:
① 非法状态无法表示(不能同时 loading 且有 data)
② 每个 action → 新状态 的映射集中可见(像一份状态转移表)
③ 新增状态时,TypeScript 会在 switch 里报错提醒处理
④ reducer 可以单独单测(不用渲染组件)
七、进阶场景
1) 撤销/重做
interface HistoryState {
past: T[];
present: T;
future: T[];
}
function historyReducer(state: HistoryState, action: HistoryAction): HistoryState {
switch (action.type) {
case 'set':
return { past: [...state.past, state.present], present: action.value, future: [] };
case 'undo': {
const prev = state.past.at(-1);
if (prev === undefined) return state;
return {
past: state.past.slice(0, -1),
present: prev,
future: [state.present, ...state.future],
};
}
case 'redo': {
const next = state.future[0];
if (next === undefined) return state;
return {
past: [...state.past, state.present],
present: next,
future: state.future.slice(1),
};
}
default:
return state;
}
}
2) 与 Immer 结合(简化深层更新)
import { produce } from 'immer';
const reducer = produce((draft: Draft, action: Action) => {
switch (action.type) {
case 'updateAddress':
draft.user.address.city = action.city; // ✅ 直接改,Immer 产生新对象
break;
case 'addTag':
draft.tags.push(action.tag);
break;
// 注意:Immer 下可以不写 default(不改就等于返回原 state)
}
});
3) 把 reducer 抽到组件外(便于测试与复用)
// reducers/cart.ts
export type CartState = { items: CartItem[]; coupon: string | null };
export type CartAction =
| { type: 'add'; item: CartItem }
| { type: 'remove'; id: string }
| { type: 'applyCoupon'; code: string };
export function cartReducer(state: CartState, action: CartAction): CartState {
// ...
}
// reducers/cart.test.ts ← 直接单测,不用渲染
test('add 会追加商品', () => {
const next = cartReducer({ items: [], coupon: null }, { type: 'add', item: mockItem });
expect(next.items).toHaveLength(1);
});
八、常见坑
// ① reducer 里直接改 state
function bad(state, action) { state.count++; return state; } // ❌ 引用不变 → 不重渲染
// ② 忘记 default 分支返回 state
function bad2(state, action) {
switch (action.type) {
case 'a': return { ...state, a: 1 };
// ❌ 未识别 action 时返回 undefined → 崩溃
}
}
// ③ 在 reducer 里做副作用
function bad3(state, action) {
api.post(state); // ❌ StrictMode 下会执行两次
return state;
}
// ④ 用随机值/时间产生状态(不确定性)
return { ...state, id: Math.random() }; // ❌
// ⑤ 期望 dispatch 有返回值
const next = dispatch({ type: 'inc' }); // ⚠️ undefined
// ⑥ 用 useReducer 管「互不相关」的多个状态
// → 拆成多个 useState 或分多个 reducer 更清晰
// ⑦ 把「所有状态」都塞进一个巨大的 reducer
// → 单个 reducer 太臃肿时,可以组合(多个 reducer 处理不同类型的 action)
// ⑧ 在 action 里传「函数」(不纯)
dispatch({ type: 'update', updater: (s) => s + 1 }); // ⚠️ 破坏可序列化与可回放
// ⑨ 依赖数组里写 dispatch(其实它稳定,可写可不写)
useEffect(() => { dispatch({ type: 'sync' }); }, [dispatch]); // 无害但多余
// ⑩ 把 useReducer 当「全局状态」用
// → 它只作用于当前组件;跨组件共享需要用 Context 传 dispatch 或用状态库
// ⑩ 的正确做法:Context + useReducer(小型全局状态)
const StoreContext = createContext<{ state: State; dispatch: Dispatch } | null>(null);
function StoreProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
// ✅ 把「值」与「dispatch」拆开可以优化性能(只需要 dispatch 的组件不会重渲染)
const value = useMemo(() => ({ state, dispatch }), [state]);
return {children} ;
}
function useStore() {
const ctx = useContext(StoreContext);
if (!ctx) throw new Error('useStore 必须在 StoreProvider 内使用');
return ctx;
}
function useDispatch() {
const ctx = useContext(StoreContext);
if (!ctx) throw new Error('useDispatch 必须在 StoreProvider 内使用');
return ctx.dispatch; // ⭐ dispatch 稳定 → 这个 Hook 的消费方永远不会因 state 变化重渲染
}
面试延伸
- 「
useReducer和useState怎么选?」
三个判断标准:① 多个状态总是一起变化、且相互关联(如表单的values/errors/touched)→useReducer;② 状态转移逻辑复杂、需要集中管理(状态机、多步骤流程、撤销重做)→useReducer;③ 只是一个独立的简单值→useState。另外useReducer的 reducer 是纯函数,可以脱离组件直接单测,这是它在复杂场景下的重要优势。 -
「为什么 reducer 必须是纯函数?」
因为 React 可能在渲染阶段调用它(包括 StrictMode 下的双调用、以及并发特性下的中断重放)。如果它不纯,就会出现:① 副作用被重复执行(重复请求、重复日志);② 相同输入得到不同结果(用Math.random()/Date.now())导致状态不稳定;③ 直接修改参数导致 React 的引用比较判断错误、UI 不更新。所以 reducer 必须「不修改参数、无副作用、结果可预测」。 -
「
useReducer的dispatch稳定吗?」
稳定。dispatch的引用在组件的整个生命周期内不变(与useState的setState一样)。所以它可以安全地放进useEffect/useCallback的依赖数组,也可以作为 props 传给子组件而不破坏React.memo。这也是「把dispatch通过 Context 分发」能获得好性能的原因——只消费dispatch的组件永远不会因为状态变化而重渲染。 -
「
dispatch怎么像setState那样基于旧值更新?」
dispatch不返回值,所以要「基于当前状态计算」时必须把计算逻辑放进 reducer:比如dispatch({ type: 'add', amount: 1 }),reducer 里用state.count + action.amount计算。这与useState的「函数式更新」思路一致——把「怎么算」交给状态的拥有者,而不是在调用方读取旧值。 -
「怎么用
useReducer实现一个状态机?」
用「判别联合 定义状态 +switch处理 action」:状态形如{ status: 'idle' | 'loading' | 'success' | 'error', ... },每个分支只携带自己需要的字段(success带data、error带error)。好处是:① 非法状态无法表示(不会出现「loading 同时有 data」);② 每个action → 新状态的映射集中可见;③ TypeScript 会在switch里强制你处理所有分支。 -
「
useReducer能替代状态管理库吗?」
在小范围内可以:用Context+useReducer就能做一个「小型全局状态」(把state与dispatch通过 Context 提供)。但它的局限是:① 没有选择器订阅(state 变化会让所有消费者重渲染,除非自己拆分 Context);② 没有中间件、DevTools、时间旅行、异步编排;③ 状态逻辑复杂后单文件会膨胀。所以跨页面、复杂异步、需要调试工具的场景仍应使用 Redux Toolkit / Zustand。 -
「
useReducer里能发请求吗?」
不能。请求是副作用,而 reducer 必须是纯函数(可能被双调用或重放)。正确做法是在事件处理函数或useEffect里发请求,然后通过dispatch把结果作为 action 送进 reducer(如dispatch({ type: 'success', data }))。这样 reducer 只负责「根据 action 计算新状态」,可测试性也最好。 -
「
useReducer的惰性初始化有什么用?」
useReducer(reducer, initialArg, init)的第三个参数init会在首次渲染时用initialArg计算初始状态,之后不再执行。它适合「初始状态需要昂贵计算」或「从localStorage/serverData构造初始状态」的场景——与useState(() => ...)的惰性初始化目的相同,只是形式不同(useReducer用第三个参数,而不是包一层函数)。
一句话速记
useReducer把「状态」与「更新规则(纯函数 reducer)」分开,适合「多状态联动、转移逻辑复杂、需要集中管理与单测」;reducer 必须纯(不改参数、无副作用、结果可预测,否则 StrictMode 双调用会出问题),必须写default返回原 state;dispatch引用稳定(可安全传给子组件与放进依赖),但不返回值——要基于旧值计算就把计算放进 reducer;用它 + 判别联合可以写状态机(非法状态无法表示);Context + useReducer能做小型全局状态(把state与dispatch拆开性能更好),复杂场景仍应上状态管理库。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!