Kodokon kodokon.com

用 useReducer 管理复杂状态

用一个纯粹的 reducer 来组织复杂状态,集中管理状态转换并排除不可能出现的组合。

9 分钟 · 3 题

在 Kodokon 中打开本课

一旦某个组件需要协调多个一起演变的状态值——请求状态、一些数据、一个错误——不断增加 useState 调用就会把状态转换逻辑分散到每一个事件处理函数里。你很清楚这个症状:要按正确的顺序调用三个 setter,迟早会出现不一致的状态,比如状态是 success 却又并排放着一个非空的错误。useReducer 把职责反了过来:状态变成单一对象,而每一次变更都通过一个由中央函数(reducer)解释的 action 来完成。

JAVASCRIPT
function requestReducer(state, action) {
  switch (action.type) {
    case "start":
      return { status: "loading", data: null, error: null };
    case "resolve":
      return {
        status: "success",
        data: action.data,
        error: null,
      };
    case "reject":
      return {
        status: "error",
        data: null,
        error: action.error,
      };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
每一次转换都重建一个完整、一致的状态。

reducer 是一个纯函数:它接收当前状态和一个 action,然后返回下一个状态,没有任何副作用。好处立竿见影:转换是显式且穷尽的,不可能的状态从源头上消失(每个 case 都重建一个一致的对象),而且逻辑集中在一处,而不是散落各处。一个有用的警示信号:如果你发现自己在两个地方复制粘贴同一串 setState 调用,那你很可能缺少一个 reducer。

JSX
function UserProfile() {
  const [state, dispatch] = useReducer(requestReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  async function load(id) {
    dispatch({ type: "start" });
    try {
      const res = await fetch("/api/users/" + id);
      const data = await res.json();
      dispatch({ type: "resolve", data });
    } catch (error) {
      dispatch({ type: "reject", error });
    }
  }

  return <button onClick={() => load(1)}>Load</button>;
}
组件表达意图,reducer 决定状态。

useStateuseReducer 之间选择并非口味问题。对简单、独立的值保留 useState;当下一个状态依赖于上一个,或多个字段一起变化时,切换到 useReducer。还有两个额外优势:第三个参数提供惰性初始化useReducer(reducer, arg, init) 只在首次渲染时运行 init),而且 React 保证 dispatch 在多次渲染之间保持相同的标识——你可以把它传给子组件而无需 useCallback

知识检测

确认你已牢记本课的重点内容。

  1. 哪个信号应当指引你选择 useReducer 而不是多个 useState 调用?
    • 组件超过一百行
    • 多个状态值一起变化,且新状态依赖于上一个状态
    • 组件接收超过三个 prop
    • 状态里包含一个数组
  2. 一个 reducer 返回它收到的状态对象,但在原地做了修改。React 会怎么做?
    • 它比较新旧引用,发现两者相等,于是跳过重新渲染
    • 它检测到深层修改并重新渲染组件
    • 它抛出错误,因为在开发环境中状态是被冻结的
  3. 为什么可以把 dispatch 传给一个被 memo 化的子组件而无需任何特别的防护?
    • 因为 React.memo 会忽略函数类型的 prop
    • 因为 dispatch 每次渲染都被重建,但结构上相等
    • 因为 React 保证 dispatch 在组件的整个生命周期内保持相同的标识