陌上人如玉
公子世无双

2.8 useReducer

2.8 useReducer

一句话: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 变化重渲染
}

面试延伸

  1. 「useReducer 和 useState 怎么选?」
    三个判断标准:① 多个状态总是一起变化、且相互关联(如表单的 values/errors/touched)→ useReducer;② 状态转移逻辑复杂、需要集中管理(状态机、多步骤流程、撤销重做)→ useReducer;③ 只是一个独立的简单值→ useState。另外 useReducer 的 reducer 是纯函数,可以脱离组件直接单测,这是它在复杂场景下的重要优势。

  2. 「为什么 reducer 必须是纯函数?」
    因为 React 可能在渲染阶段调用它(包括 StrictMode 下的双调用、以及并发特性下的中断重放)。如果它不纯,就会出现:① 副作用被重复执行(重复请求、重复日志);② 相同输入得到不同结果(用 Math.random()/Date.now())导致状态不稳定;③ 直接修改参数导致 React 的引用比较判断错误、UI 不更新。所以 reducer 必须「不修改参数、无副作用、结果可预测」。

  3. 「useReducer 的 dispatch 稳定吗?」
    稳定。dispatch 的引用在组件的整个生命周期内不变(与 useState 的 setState 一样)。所以它可以安全地放进 useEffect / useCallback 的依赖数组,也可以作为 props 传给子组件而不破坏 React.memo。这也是「把 dispatch 通过 Context 分发」能获得好性能的原因——只消费 dispatch 的组件永远不会因为状态变化而重渲染。

  4. 「dispatch 怎么像 setState 那样基于旧值更新?」
    dispatch 不返回值,所以要「基于当前状态计算」时必须把计算逻辑放进 reducer:比如 dispatch({ type: 'add', amount: 1 }),reducer 里用 state.count + action.amount 计算。这与 useState 的「函数式更新」思路一致——把「怎么算」交给状态的拥有者,而不是在调用方读取旧值。

  5. 「怎么用 useReducer 实现一个状态机?」
    用「判别联合 定义状态 + switch 处理 action」:状态形如 { status: 'idle' | 'loading' | 'success' | 'error', ... },每个分支只携带自己需要的字段(success 带 data、error 带 error)。好处是:① 非法状态无法表示(不会出现「loading 同时有 data」);② 每个 action → 新状态 的映射集中可见;③ TypeScript 会在 switch 里强制你处理所有分支。

  6. 「useReducer 能替代状态管理库吗?」
    在小范围内可以:用 Context + useReducer 就能做一个「小型全局状态」(把 state 与 dispatch 通过 Context 提供)。但它的局限是:① 没有选择器订阅(state 变化会让所有消费者重渲染,除非自己拆分 Context);② 没有中间件、DevTools、时间旅行、异步编排;③ 状态逻辑复杂后单文件会膨胀。所以跨页面、复杂异步、需要调试工具的场景仍应使用 Redux Toolkit / Zustand。

  7. 「useReducer 里能发请求吗?」
    不能。请求是副作用,而 reducer 必须是纯函数(可能被双调用或重放)。正确做法是在事件处理函数或 useEffect 里发请求,然后通过 dispatch 把结果作为 action 送进 reducer(如 dispatch({ type: 'success', data }))。这样 reducer 只负责「根据 action 计算新状态」,可测试性也最好。

  8. 「useReducer 的惰性初始化有什么用?」
    useReducer(reducer, initialArg, init) 的第三个参数 init 会在首次渲染时用 initialArg 计算初始状态,之后不再执行。它适合「初始状态需要昂贵计算」或「从 localStorage / serverData 构造初始状态」的场景——与 useState(() => ...) 的惰性初始化目的相同,只是形式不同(useReducer 用第三个参数,而不是包一层函数)。

一句话速记

useReducer 把「状态」与「更新规则(纯函数 reducer)」分开,适合「多状态联动、转移逻辑复杂、需要集中管理与单测」;reducer 必须纯(不改参数、无副作用、结果可预测,否则 StrictMode 双调用会出问题),必须写 default 返回原 state;dispatch 引用稳定(可安全传给子组件与放进依赖),但不返回值——要基于旧值计算就把计算放进 reducer;用它 + 判别联合可以写状态机(非法状态无法表示);Context + useReducer 能做小型全局状态(把 state 与 dispatch 拆开性能更好),复杂场景仍应上状态管理库。

赞(0) 打赏
未经允许不得转载:陌上寒 » 2.8 useReducer

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫