【React State】告别 `useState` 滥用:何时应该选择 `useReducer`

发布于:2025-08-20 ⋅ 阅读:(12) ⋅ 点赞:(0)

【React State】告别 useState 滥用:何时应该选择 useReducer

所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【React 性能】性能优化第一课:搞懂 React.memo, useCallback, useMemo
作者: 码力无边


引言:你的组件状态,是不是一团“意大利面”?

嘿,各位在 React Hooks 的世界里构建奇妙应用的前端道友们,我是码力无边

自从 React Hooks 横空出世,useState 就如同我们修仙路上的第一把“新手木剑”。它简单、直接、锋利,几乎能解决我们遇到的 80% 的状态管理问题。

const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

三下五除二,几个 useState 一摆,组件的状态就管理起来了。对于简单的组件,这套“剑法”行云流水,好不快哉!

但随着我们修为的加深,要面对的“妖魔”(业务逻辑)也越来越强大。你的组件状态不再是简单的数字或字符串,而是一个包含了多个子值、彼此之间还可能有关联的复杂对象。此时,如果你还固执地使用“新手木剑”,你的组件很快就会变成这样:

function ComplexForm() {
  const [username, setUsername] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [step, setStep] = useState(1);

  const handleSubmit = () => {
    setIsLoading(true);
    setError(null);
    // ... 发送请求
    // 成功后...
    setIsLoading(false);
    setStep(2);
    // 失败后...
    setIsLoading(false);
    setError('请求失败...');
  };
  // ...
}

看到问题了吗?

  1. 状态“碎片化”:多个看似独立、实则相关的状态被分散在各处。
  2. 更新逻辑“意大利面化”:一个 handleSubmit 函数里,包含了多个 setXXX 调用。这些状态更新逻辑散落在组件的各个事件处理函数中,难以追踪和维护。当逻辑变得更复杂时(比如,根据错误类型显示不同信息),这里的代码会迅速膨胀成一坨难以理解的“意大利面”。

当你的“新手木剑”已经舞不动这日益复杂的“剑招”时,是时候请出 React 为我们准备的另一把“上古神兵”了——useReducer

useReducer 就像是一本高级“剑谱”,它要求你将**“如何更新状态”的逻辑**(剑招)从组件中分离出来,让状态的流转变得清晰、可预测、易于测试。

今天,码力无边就将带你深入这本“剑谱”,让你明白 useReducer 并非 useState 的替代品,而是处理复杂状态场景的“最优解”。你将学会何时应该放下手中的“木剑”,并自信地挥舞起 useReducer 这把神兵。

一、useReducer 的“内功心法”:三要素

初见 useReducer,你可能会觉得它比 useState 繁琐。但它的“繁琐”恰恰是其强大之所在。它由三个核心要素构成:

1. State (状态):和 useState 一样,是你需要管理的数据。但通常,它是一个包含了多个子值的对象

2. Action (动作):一个描述“发生了什么”的普通 JavaScript 对象。按照约定,它通常有一个 type 属性(字符串,描述动作类型)和一个可选的 payload 属性(携带的数据)。

  • { type: 'INCREMENT' }
  • { type: 'UPDATE_FIELD', payload: { field: 'username', value: '码力无边' } }

3. Reducer (归纳函数):这是一个纯函数,是 useReducer 的灵魂。它接收当前 state 和一个 action 作为参数,然后返回一个新的 state

  • (state, action) => newState
  • 它的作用就像一个“状态管理员”,根据收到的“指令” (action),来决定如何基于旧状态 (state) 计算出新状态。它只负责计算,不产生副作用(比如 API 请求)
useReducer 的基本用法

让我们来看一个经典的计数器例子,感受一下它的运作流程:

import React, { useReducer } from 'react';

// 3. 定义 Reducer 函数
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload };
    default:
      throw new Error();
  }
};

function Counter() {
  // 1. 定义初始 State
  const initialState = { count: 0 };
  
  // useReducer 接收 reducer 函数和初始 state
  // 返回当前 state 和一个 dispatch 函数
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      {/* 2. 通过 dispatch 发送 Action */}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
    </>
  );
}

流程解析:

  1. 用户点击 “+” 按钮。
  2. onClick 事件被触发,调用 dispatch({ type: 'increment' })
  3. dispatch 函数将这个 action 对象“派发”给 reducer 函数。
  4. React 调用 reducer(currentState, { type: 'increment' })
  5. reducer 内部的 switch 语句匹配到 'increment',执行 return { count: state.count + 1 }
  6. useReducer 接收到 reducer 返回的新 state,用它来更新组件的 state,并触发重新渲染。

二、何时拔出 useReducer 这把神兵?

现在你已经了解了 useReducer 的基本招式。但一个高手,重在懂得“时机”。什么时候,我们应该果断放弃 useState,转而使用 useReducer 呢?

时机一:当 State 是一个复杂的对象或数组时

当你的 state 包含了多个相互关联的子值时,useReducer 可以让你将这些状态聚合管理,避免声明一大堆 useState

让我们重构文章开头的那个复杂表单:

改造前 (useState 版本):

  • 状态分散,更新逻辑混乱。

改造后 (useReducer 版本):

const initialState = {
  username: '',
  email: '',
  password: '',
  isLoading: false,
  error: null,
  step: 1,
};

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.payload.field]: action.payload.value };
    case 'SUBMIT_START':
      return { ...state, isLoading: true, error: null };
    case 'SUBMIT_SUCCESS':
      return { ...state, isLoading: false, step: 2 };
    case 'SUBMIT_FAILURE':
      return { ...state, isLoading: false, error: action.payload };
    default:
      return state;
  }
}

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const handleFieldChange = (field, value) => {
    dispatch({ type: 'UPDATE_FIELD', payload: { field, value } });
  };
  
  const handleSubmit = () => {
    dispatch({ type: 'SUBMIT_START' });
    api.submitForm({ /* ... */ })
      .then(() => dispatch({ type: 'SUBMIT_SUCCESS' }))
      .catch(err => dispatch({ type: 'SUBMIT_FAILURE', payload: err.message }));
  };
  
  // ... render logic using state.username, state.isLoading etc.
}

优势体现:

  • 状态聚合:所有表单相关的状态都集中在 state 对象中,一目了然。
  • 逻辑集中:所有状态更新的逻辑都收敛到了 formReducer 内部。handleSubmit 函数的职责变得非常清晰:只负责触发“开始”、“成功”、“失败”这些高级别的“意图”,而不关心具体的 state 如何变化
  • 可读性增强:读 dispatch({ type: 'SUBMIT_SUCCESS' }) 远比读一堆 setIsLoading(false); setStep(2); 要清晰得多。
时机二:当下一个 State 依赖于前一个 State 时

我们知道,useStatesetState 函数也可以接收一个函数来安全地更新依赖于前一个 state 的值,比如 setCount(c => c + 1)

useReducer 在这方面做得更彻底。因为 reducer 函数总是能接收到最新的 state,所以处理复杂的、依赖于前序状态的逻辑转换变得非常自然和安全。

场景: 实现一个带“撤销 (Undo)”功能的计数器。

const initialState = {
  count: 0,
  history: [],
};

function undoableReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        count: state.count + 1,
        history: [...state.history, state.count], // 把旧的 count 存入历史
      };
    case 'undo':
      if (state.history.length === 0) return state; // 没有历史记录
      const previousCount = state.history[state.history.length - 1];
      return {
        ...state,
        count: previousCount,
        history: state.history.slice(0, -1), // 移除最后一条历史
      };
    default:
      return state;
  }
}

useState 来实现这个逻辑,你需要维护两个 useState (counthistory),并且在更新 count 的同时,还要小心翼翼地同步更新 history,非常容易出错。而 useReducer 将这个原子性的、依赖性的更新封装得天衣无缝。

时机三:当你想优化深层组件的性能时

这是一个非常重要但经常被忽略的优势。

场景: 一个深层嵌套的组件树,顶层组件 App 有一个状态,而最底层的组件 DeepChild 需要修改这个状态。

useState 的方案:
App 需要把 setState 函数一层一层地通过 props 传递下去 (App -> Middle -> DeepChild)。

function App() {
  const [theme, setTheme] = useState('light');
  return <Middle setTheme={setTheme} />;
}
function Middle({ setTheme }) { return <DeepChild setTheme={setTheme} />; }
function DeepChild({ setTheme }) { return <button onClick={() => setTheme('dark')}>Change</button>; }

问题在于,setTheme 函数的引用地址在每次 App 渲染时可能会变(取决于 React 的实现细节,但我们不应依赖它),这会导致 MiddleDeepChild 即使被 React.memo 包裹,也可能因为 setTheme prop 的变化而重新渲染。

useReducer 的方案:
useReducer 返回的 dispatch 函数,React 保证其引用地址在组件的整个生命周期内是稳定的

const reducer = (state, action) => { /* ... */ };
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  // dispatch 的引用是稳定的!
  return <Middle dispatch={dispatch} />;
}
function Middle({ dispatch }) { return <DeepChild dispatch={dispatch} />; }
function DeepChild({ dispatch }) { return <button onClick={() => dispatch({ type: 'CHANGE_THEME' })}>Change</button>; }

现在,你可以放心地用 React.memo 包裹 MiddleDeepChild,因为传递给它们的 dispatch prop 永远不会变,它们不会因为父组件的渲染而跟着不必要地渲染。

当然,这个问题也可以用 useCallback 来包装 setTheme 函数解决,但 useReducer 提供了一种更“原生”、更具结构化的解决方案。当结合 Context API 进行全局状态管理时,这个优势会更加明显。

写在最后:useReducer 是一种思想的转变

useStateuseReducer 并非“非黑即白”的对立关系,它们是 React 工具箱中针对不同场景的两种工具。

  • useState 是处理局部、简单、独立状态的“手枪”,轻便快捷。
  • useReducer 是处理复杂、关联、跨组件状态的“狙击枪”,精准、强大、有章法。

useState 转向 useReducer,不仅仅是 API 的替换,更是一种思想的转变

  • 命令式(“我要把 isLoading 设为 true,把 error 设为 null”)到声明式(“我要发起一个‘提交开始’的动作”)。
  • 将状态更新逻辑与组件的视图逻辑耦合将它们彻底分离

掌握 useReducer,意味着你开始以一种更宏观、更具架构性的视角来思考组件的状态管理。你的组件将变得更具可读性、可预测性、可测试性,也更能从容地应对未来日益复杂的业务需求。这,就是从“会用 React”到“精通 React”的一次重要跃迁。


专栏预告与互动:

我们已经掌握了 React 核心的状态管理工具。但在实际开发中,我们往往需要封装自己的逻辑,让它们能在多个组件之间复用。这就是自定义 Hooks 的魅力所在。

下一篇,我们将进入 React Hooks 的封装艺术,学习如何编写高质量的自定义 Hooks,将你的重复逻辑抽象成可复用的“超能力”,极大提升你的开发效率和代码质量!

感觉码力无边的“状态管理剑谱”让你茅塞顿开?点赞、收藏、关注,你的每一次支持,都是我绘制下一幅“武功图谱”的灵感源泉!

今日论道: Redux 是一个非常流行的 React 全局状态管理库,它的核心思想(State, Action, Reducer)和 useReducer 几乎如出一辙。你认为在只有中小型状态管理需求的场景下,useReducer + Context API 的组合能否完全替代 Redux?为什么?在评论区分享你的看法,我们一起探讨!


网站公告

今日签到

点亮在社区的每一天
去签到