如何像专业人士一样使用 React useReducer hook

2025-05-28

如何像专业人士一样使用 React useReducer hook

在开发 React 网站时,管理 React 中的状态是主要问题之一。useState这当然是创建和管理(函数式)React 组件状态的最常用方法。但你知道吗,这useReducer其实是一个非常强大的替代方案。

还有许多库提供自定的方式来管理你的整个(或部分)状态,比如ReduxMobxRecoilXState

但在选择一个库来帮助你管理状态问题之前,你应该了解 React 中另一种原生的状态管理方法:useReducer。如果以正确的方式并出于正确的目的使用,它可以非常强大。事实上,它非常强大,以至于著名的 Redux 库可以被认为是一个大型的、经过优化的库useReducer(正如我们即将看到的)。

在本文中,我们将首先解释什么useReducer是 Swift 以及如何使用它,并为您提供良好的思维模型和示例。然后,我们将进行 SwiftuseState与 Swift 的useReducer比较,以了解何时使用 Swift。

对于 TypeScript 用户,我们还将了解如何useReducer一起使用 TypeScript 和。

让我们开始吧!

什么是 React useReducerHook 以及如何使用它

正如简介中提到的,useStateuseReducer是 React 中两种原生的状态管理方式。你可能已经非常熟悉前者,因此从这里开始理解 会很有帮助useReducer

useStateuseReducer:快速比较

乍一看,它们非常相似。让我们并排比较一下:

const [state, setState] = useState(initialValue);

const [state, dispatch] = useReducer(reducer, initialValue);
Enter fullscreen mode Exit fullscreen mode

如你所见,这两种情况下,钩子都会返回一个包含两个元素的数组。第一个是state,第二个是允许你修改状态的函数:setStateforuseStatedispatchfor 。我们稍后useReducer会了解其工作原理。dispatch

useState和都提供了初始状态useReducer。钩子参数的主要区别在于reducer提供给 的useReducer

现在,我先简单介绍一下这个reducer函数,它负责处理状态更新的逻辑。我们稍后会在文章中详细了解它。

setState现在让我们看看如何使用或 来改变状态dispatch。为此,我们将使用经过验证的计数器示例 - 我们希望在单击按钮时将其加一:

// with `useState`
<button onClick={() => setCount(prevCount => prevCount + 1)}>
  +
</button>

// with `useReducer`
<button onClick={() => dispatch({type: 'increment', payload: 1})}>
  +
</button>
Enter fullscreen mode Exit fullscreen mode

虽然useState您可能对该版本很熟悉(如果不熟悉,可能是因为我们使用的是功能更新形式setState),但该useReducer版本可能看起来有点奇怪。

为什么要传递一个带有属性的对象typepayload这些(神奇的)值又'increment'从何而来?别担心,谜底会揭晓!

目前,您可以注意到两个版本仍然非常相似。无论哪种情况,您都可以通过调用更新函数(setStatedispatch)来更新状态,并提供关于如何更新状态的具体信息。

现在让我们从高层次上探讨该useReducer版本的具体工作原理。

useReducer:后端心智模型

在本节中,我想给你一个关于useReducer钩子工作原理的良好思维模型。这一点很重要,因为当我们深入实现细节时,事情可能会变得有点不知所措。尤其是如果你以前从未使用过类似的结构。

一种思考方式useReducer是将其视为后端。这听起来可能有点奇怪,但请耐心听我说:我非常喜欢这个类比,而且我认为它很好地解释了 Reducer。

后端通常采用某种方式来保存数据(数据库)以及允许您修改数据库的 API。

该 API 包含可供调用的 HTTP 端点。GET 请求允许您访问数据,POST 请求允许您修改数据。发送 POST 请求时,您还可以提供一些参数;例如,如果您想创建一个新用户,通常会在 HTTP POST 请求中包含该新用户的用户名、电子邮件地址和密码。

useReducer那么,它与后端有何相似之处?嗯:

  • state是数据库。它存储你的数据。
  • dispatch相当于调用API端点来修改数据库。
    • type您可以通过指定呼叫来选择要呼叫的端点。
    • 您可以使用属性提供与POST 请求payload相对应的附加数据。body
    • 和 都是type赋予payload的对象的属性reducer。该对象称为action
  • reducer是 API 的逻辑。当后端收到 API 调用(调用dispatch)时,它会被调用,并处理如何根据端点和请求内容(action)更新数据库。

这是一个完整的使用示例useReducer。请花点时间仔细理解,并将其与上面描述的后端思维模型进行比较。

import { useReducer } from 'react';

// initial state of the database
const initialState = { count: 0 };

// API logic: how to update the database when the
// 'increment' API endpoint is called
const reducer = (state, action) => {
  if (action.type === 'increment') {
    return { count: state.count + action.payload };
  }
};

function App() {
  // you can think of this as initializing and setting
  // up a connection to the backend
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {/* Reading from the database */}
      Count: {state.count}
      {/* calling the API endpoint when the button is clicked */}
      <button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
        +
      </button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

你能看出这两者有何关联吗?

请记住,上面的代码不应该在生产环境中使用。它是一个最小化版本的useReducer钩子,用于帮助您将其与后端思维模型进行比较,但它缺少一些您将在本文中了解的重要内容。

现在(希望)您已经很好地了解了useReducer高层次的工作方式,让我们进一步探讨细节。

减速机的工作原理

我们将首先处理减速器,因为它是主要逻辑发生的地方。

你可能已经从上面的例子中注意到,reducer 是一个接受两个参数的函数。第一个是 current state,第二个是 the action(在我们的后端类比中,它对应于 API 端点 + 请求可能包含的任何主体)。

请记住,您永远不必亲自向 Reducer 提供参数。这将由useReducerHook 自动处理:状态已知,并且action只是将 的参数dispatch作为第二个参数传递给 Reducer。

的格式state可以是任何你想要的(通常是一个对象,但实际上可以是任何东西)。 的格式action也可以是任何你想要的,但是有一些非常常用的构造约定,我建议你遵循这些约定——我们稍后会学习它们。至少在你熟悉这些约定并确信你真正想要的不是这些约定之前。

因此按照惯例,这action是一个具有一个必需属性和一个可选属性的对象:

  • type是必需属性(类似于 API 端点)。它告诉 Reducer 应该使用什么逻辑来修改状态。
  • payload是可选属性(类似于 HTTP POST 请求的主体,如果有的话)。它向 Reducer 提供有关如何修改状态的附加信息。

在我们之前的计数器示例中,state是一个具有单一count属性的对象。是一个可以是action的对象,其有效载荷是您想要增加计数器的量。type'increment'

// this is an example `state`
const state = { count: 0 };

// this is an example `action`
const action = { type: 'increment', payload: 2 };
Enter fullscreen mode Exit fullscreen mode

switchReducer 通常由动作语句构成type,例如:

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload };
    case 'decrement':
      return { count: state.count - action.payload };
    case 'reset':
      return { count: 0 };
  }
};
Enter fullscreen mode Exit fullscreen mode

在此示例中,reducer 接受三种 action 类型:“increment”、“decrement”和“reset”。“increment”和“decrement”都需要 action 的有效负载,用于确定计数器增加或减少的量。相反,“reset”类型不需要任何有效负载,因为它会将计数器重置回 0。

这是一个非常简单的示例,实际应用中的 Reducer 通常要庞大和复杂得多。我们将在后续章节中探讨改进 Reducer 编写方法的方法,并给出实际应用中 Reducer 的示例。

调度功能如何运作?

如果你已经理解了 Reducer 的工作原理,那么理解调度函数就非常简单了。

dispatch调用时传入的任何参数都将作为reducer函数的第二个参数(action)。按照惯例,该参数是一个带有type和可选 的对象payload,正如我们在上一节中看到的那样。

使用我们最后一个 reducer 示例,如果我们想要制作一个按钮,在点击时将计数器减少 2,它看起来会像这样:

<button onClick={() => dispatch({ type: 'decrement', payload: 2 })}>
  -
</button>
Enter fullscreen mode Exit fullscreen mode

如果我们想要一个按钮将计数器重置为 0,仍然使用我们的最后一个例子,你可以省略payload

<button onClick={() => dispatch({ type: 'reset' })}>
  reset
</button>
Enter fullscreen mode Exit fullscreen mode

需要注意的一点dispatch是,React 保证其标识在渲染之间不会改变。这意味着你不需要将其放入依赖数组中(即使你这么做了,它也不会触发依赖数组)。这与setState中的函数行为相同useState

如果您对最后一段有点困惑,我已经为您准备了有关依赖数组的这篇文章!

useReducer初始状态

到目前为止我们还没有多次提到它,但它useReducer也接受第二个参数,即您想要赋予的初始值state

它本身并不是一个必需的参数,但如果您不提供它,状态将会是undefined最初的,而这很少是您想要的。

通常在初始状态中定义 Reducer 状态的完整结构。它通常是一个对象,不应在 Reducer 内部向该对象添加新属性。

在我们的反例中,初始状态很简单:

// initial state of the database
const initialState = { count: 0 };

· · ·

// usage inside of the component
const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode

我们以后会看到更多这样的例子。

useReducer技巧和窍门

我们可以通过多种方式来改进 的使用useReducer。其中一些是你真正应该做的事情,而其他一些则更多地取决于个人喜好。

我粗略地将它们从重要性到可选性进行了分类,从最重要的开始。

Reducer 应该针对未知的操作类型抛出错误

在我们的反例中,我们有一个 switch 语句,其中包含三个 case:“increment”、“decrement”和“reset”。如果你真的把这段代码写进了你的代码编辑器,你可能会发现 ESLint 对你很生气。

你有ESLint吗?如果没有,你真的应该设置一下!

ESLint 确实希望 switch 语句有一个默认情况。那么,当 Reducer 处理未知的 action 类型时,它的默认情况应该是什么呢?

有些人喜欢简单地返回状态:

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload };
    case 'decrement':
      return { count: state.count - action.payload };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

但我真的不喜欢这样。要么这个 action 类型是你期望的,并且应该有对应的 case,要么它不是,返回的也不state是你想要的。这基本上会在提供错误的 action 类型时产生一个静默错误,而静默错误很难调试。

相反,您的默认 Reducer 案例应该抛出一个错误:

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload };
    case 'decrement':
      return { count: state.count - action.payload };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

这样,您就不会错过拼写错误或忘记案例。

你应该在每一个行动中传播国家

到目前为止,我们只看到了一个非常简单的useReducer例子,其中状态是一个只有一个属性的对象。通常情况下,useReducer用例要求状态对象至少具有几个属性。

一种常见的useReducer用法是处理表单。下面是一个包含两个输入字段的示例,但您可以想象,如果包含更多字段,情况也是如此。

(注意!下面的代码有一个错误。你能发现它吗?)

import { useReducer } from 'react';

const initialValue = {
  username: '',
  email: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'username':
      return { username: action.payload };
    case 'email':
      return { email: action.payload };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);
  return (
    <div>
      <input
        type="text"
        value={state.username}
        onChange={(event) =>
          dispatch({ type: 'username', payload: event.target.value })
        }
      />
      <input
        type="email"
        value={state.email}
        onChange={(event) =>
          dispatch({ type: 'email', payload: event.target.value })
        }
      />
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

该错误出现在 reducer 中:更新username将完全覆盖之前的状态并删除email(并且更新email将对 执行相同的操作username)。

解决这个问题的方法是记住每次更新属性时都保留所有先前的状态。这可以通过扩展语法轻松实现:

import { useReducer } from 'react';

const initialValue = {
  username: '',
  email: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'username':
      return { ...state, username: action.payload };
    case 'email':
      return { ...state, email: action.payload };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);
  return (
    <div>
      <input
        value={state.username}
        onChange={(event) =>
          dispatch({ type: 'username', payload: event.target.value })
        }
      />
      <input
        value={state.email}
        onChange={(event) =>
          dispatch({ type: 'email', payload: event.target.value })
        }
      />
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

这个例子实际上还可以进一步优化。你可能已经注意到,我们在 Reducer 中重复了一些代码: 和usernameemail情况本质上逻辑相同。对于两个字段来说,这还不算太糟,但我们可以有更多字段。

有一种方法可以重构代码,使所有输入仅执行一个操作,即使用ES2015 的计算键功能

import { useReducer } from 'react';

const initialValue = {
  username: '',
  email: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'textInput':
      return {
        ...state,
        [action.payload.key]: action.payload.value,
      };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);
  return (
    <div>
      <input
        value={state.username}
        onChange={(event) =>
          dispatch({
            type: 'textInput',
            payload: { key: 'username', value: event.target.value },
          })
        }
      />
      <input
        value={state.email}
        onChange={(event) =>
          dispatch({
            type: 'textInput',
            payload: { key: 'email', value: event.target.value },
          })
        }
      />
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

如您所见,我们现在只剩下一种操作类型: 。操作有效负载也发生了变化 - 它已成为具有(要更新的属性)和(要更新的值)textInput的对象keyvaluekey

如果你问我的话,这真是太棒了!

你可能会注意到,这段代码中还有一个地方重复了:onChange事件处理程序。唯一改变的是payload.key

事实上,您可以进一步将其提取为可重复使用的操作,您只需提供即可key

我倾向于仅当减速器开始变得非常大时,或者当非常相似的动作被重复多次时才使用可重复使用的动作。

这是一种非常常见的模式,我们将在本文后面展示一个示例。

遵循常规动作结构

我所说的“常规动作结构”是我们迄今为止在本文中一直使用的结构:action应该是一个具有必需type和可选的对象文字payload

这是 Redux 构建 Action 的方式,也是最常用的方式。它已经过实践检验,是所有useReducerAction 的默认设置。

这种结构的主要缺点是有时会有点冗长。但除非你非常习惯,否则useReducer我建议你坚持使用 Redux 的方式。

糖语法:解构typepayload从动作

这关乎生活质量。与其在 Reducer 中到处重复action.payload(并且可能重复action.type),不如直接解构 Reducer 的第二个参数,如下所示:

const reducer = (state, { type, payload }) => {
  switch (type) {
    case 'increment':
      return { count: state.count + payload };
    case 'decrement':
      return { count: state.count - payload };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error(`Unknown action type: ${type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

你甚至可以更进一步,解构状态。这只有在你的 Reducer 状态足够小时才有用,但在这种情况下效果会很好。

const reducer = ({ count }, { type, payload }) => {
  switch (type) {
    case 'increment':
      return { count: count + payload };
    case 'decrement':
      return { count: count - payload };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error(`Unknown action type: ${type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

这就是所有的技巧和窍门!

useReducer第三个参数:延迟初始化

值得一提的是,它useReducer还有一个可选的第三个参数。该参数是一个函数,用于在需要时延迟初始化状态。

虽然不太常用,但真正需要的时候还是很有用的。React 文档里有一个很好的例子,演示了如何使用延迟初始化

useStatevs useReducer:何时使用哪个

现在你已经了解了它的useReducer工作原理以及如何在组件中使用它,我们需要解决一个重要的问题。由于useStateuseReducer是两种管理状态的方式,那么在什么情况下应该选择哪一种呢?

这类问题总是比较棘手,因为答案通常会根据提问对象而变化,而且高度依赖于具体情况。不过,还是有一些指导原则可以指导你做出选择。

首先,要知道useState应该仍然是你管理 React 状态的默认选择。只有useReducer当你在使用 时遇到问题useState(并且可以通过切换到 解决useReducer)时才切换到 。至少在你拥有足够的经验,useReducer能够提前知道应该使用哪个之前,不要切换到 。

我将通过几个例子来说明何时使用useReducerover 。useState

相互依赖的多个状态

一个很好的用例useReducer是当您有多个相互依赖的状态时。

在创建表单时,这种情况很常见。假设你有一个文本输入框,并且想要跟踪三件事:

  1. 输入的值。
  2. 输入框是否已被用户“触摸”。这对于判断是否显示错误非常有用。例如,如果该字段是必填字段,则您希望在字段为空时显示错误。但是,如果用户之前从未访问过该输入框,则您不希望在首次渲染时显示错误。
  3. 是否有错误。

使用时useState,您必须使用钩子三次,并在每次发生变化时分别更新三种状态。

有了useReducer,逻辑实际上非常简单:

import { useReducer } from 'react';

const initialValue = {
  value: '',
  touched: false,
  error: null,
};

const reducer = (state, { type, payload }) => {
  switch (type) {
    case 'update':
      return {
        value: payload.value,
        touched: true,
        error: payload.error,
      };
    case 'reset':
      return initialValue;
    default:
      throw new Error(`Unknown action type: ${type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);
  console.log(state);
  return (
    <div>
      <input
        className={state.error ? 'error' : ''}
        value={state.value}
        onChange={(event) =>
          dispatch({
            type: 'update',
            payload: {
              value: event.target.value,
              error: state.touched ? event.target.value.length === 0 : null,
            },
          })
        }
      />
      <button onClick={() => dispatch({ type: 'reset' })}>reset</button>
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

添加一些基本的 CSS 来设置error类的样式,这样你就可以获得具有良好用户体验和简单逻辑的输入,这要归功于useReducer

.error {
  border-color: red;
}

.error:focus {
  outline-color: red;
}
Enter fullscreen mode Exit fullscreen mode

管理复杂状态

另一个很好的用例useReducer是当您拥有大量不同的状态时,将它们全部放入useState会变得非常失控。

我们之前看到过一个示例,单个 Reducer 管理 2 个输入,并执行相同的 Action。我们可以轻松地将该示例扩展至 4 个输入。

当我们这样做的时候,我们不妨重构每个人的动作input

import { useReducer } from 'react';

const initialValue = {
  firstName: '',
  lastName: '',
  username: '',
  email: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'update':
      return {
        ...state,
        [action.payload.key]: action.payload.value,
      };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);

  const inputAction = (event) => {
    dispatch({
      type: 'update',
      payload: { key: event.target.name, value: event.target.value },
    });
  };

  return (
    <div>
      <input
        value={state.firstName}
        type="text"
        name="firstName"
        onChange={inputAction}
      />
      <input
        value={state.lastName}
        type="text"
        name="lastName"
        onChange={inputAction}
      />
      <input
        value={state.username}
        type="text"
        onChange={inputAction}
        name="username"
      />
      <input
        value={state.email}
        type="email"
        name="email"
        onChange={inputAction}
      />
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

说真的,这段代码有多简洁明了?想象一下,如果用 4 个输入来做会怎么样useState!好吧,这确实没那么糟糕,但它可以扩展到你想要的输入数量,除了输入本身,不需要添加任何其他东西。

您还可以在此基础上轻松构建。例如,我们可能希望将上一节的touchedanderror属性添加到本节的四个输入中。

事实上,我建议你自己尝试一下,这是一个很好的练习,可以巩固你迄今为止所学到的知识!

那么如果这样做,用什么useState代替呢?

摆脱十几个useState语句的一种方法是将所有状态放入存储在单个对象中useState,然后更新它。

这个解决方案确实有效,有时也挺好用。但你经常会发现自己重新实现了一个useReducer更别扭的方法。不如直接用 Reducer 吧。

useReducer使用 TypeScript

好了,你现在应该掌握了窍门useReducer。如果你是 TypeScript 用户,你可能想知道如何让两者完美兼容。

还好,这很简单。如下:

import { useReducer, ChangeEvent } from 'react';

type State = {
  firstName: string;
  lastName: string;
  username: string;
  email: string;
};

type Action =
  | {
      type: 'update';
      payload: {
        key: string;
        value: string;
      };
    }
  | { type: 'reset' };

const initialValue = {
  firstName: '',
  lastName: '',
  username: '',
  email: '',
};

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'update':
      return { ...state, [action.payload.key]: action.payload.value };
    case 'reset':
      return initialValue;
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);

  const inputAction = (event: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: 'update',
      payload: { key: event.target.name, value: event.target.value },
    });
  };

  return (
    <div>
      <input
        value={state.firstName}
        type="text"
        name="firstName"
        onChange={inputAction}
      />
      <input
        value={state.lastName}
        type="text"
        name="lastName"
        onChange={inputAction}
      />
      <input
        value={state.username}
        type="text"
        onChange={inputAction}
        name="username"
      />
      <input
        value={state.email}
        type="email"
        name="email"
        onChange={inputAction}
      />
    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

如果您不熟悉该类型的语法Action,它是一个可区分的联合

Redux:一个过于强大的useReducer

我们的指南即将结束useReducer(呼,比我预想的要长得多!)。还有一件重要的事情要提一下:Redux

你可能听说过 Redux,它是一个非常流行的状态管理库。有人讨厌它,有人喜欢它。但事实证明,你之前的所有理解都useReducer对理解 Redux 大有裨益。

事实上,你可以把 Redux 看作是一个大型的、全局的、可管理和优化的useReducer整个应用程序。它就是这样的。

你有一个“store”,也就是你的状态,然后你定义“actions”来告诉“reducer”如何修改这个 store。听起来很熟悉!

当然,它们之间存在一些重要的区别,但是如果您理解得useReducer很好,那么您就可以轻松理解 Redux。

包起来

本文到此结束!希望它能帮助你了解所有你想了解的内容useReducer

正如您所见,它可以成为您的 React 工具包中非常强大的工具。

祝你好运!

文章来源:https://dev.to/pierreouannes/how-to-use-react-usereducer-hook-like-a-pro-3ll3
PREV
我读过……《程序员修炼之道》
NEXT
GitHub 上 5.4 万颗星是如何流失的