如何像专业人士一样使用 React useReducer hook
在开发 React 网站时,管理 React 中的状态是主要问题之一。useState
这当然是创建和管理(函数式)React 组件状态的最常用方法。但你知道吗,这useReducer
其实是一个非常强大的替代方案。
还有许多库提供自定的方式来管理你的整个(或部分)状态,比如Redux、Mobx、Recoil或XState。
但在选择一个库来帮助你管理状态问题之前,你应该了解 React 中另一种原生的状态管理方法:useReducer
。如果以正确的方式并出于正确的目的使用,它可以非常强大。事实上,它非常强大,以至于著名的 Redux 库可以被认为是一个大型的、经过优化的库useReducer
(正如我们即将看到的)。
在本文中,我们将首先解释什么useReducer
是 Swift 以及如何使用它,并为您提供良好的思维模型和示例。然后,我们将进行 SwiftuseState
与 Swift 的useReducer
比较,以了解何时使用 Swift。
对于 TypeScript 用户,我们还将了解如何useReducer
一起使用 TypeScript 和。
让我们开始吧!
什么是 React useReducer
Hook 以及如何使用它
正如简介中提到的,useState
和useReducer
是 React 中两种原生的状态管理方式。你可能已经非常熟悉前者,因此从这里开始理解 会很有帮助useReducer
。
useState
和useReducer
:快速比较
乍一看,它们非常相似。让我们并排比较一下:
const [state, setState] = useState(initialValue);
const [state, dispatch] = useReducer(reducer, initialValue);
如你所见,这两种情况下,钩子都会返回一个包含两个元素的数组。第一个是state
,第二个是允许你修改状态的函数:setState
foruseState
和dispatch
for 。我们稍后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>
虽然useState
您可能对该版本很熟悉(如果不熟悉,可能是因为我们使用的是的功能更新形式setState
),但该useReducer
版本可能看起来有点奇怪。
为什么要传递一个带有属性的对象type
?payload
这些(神奇的)值又'increment'
从何而来?别担心,谜底会揭晓!
目前,您可以注意到两个版本仍然非常相似。无论哪种情况,您都可以通过调用更新函数(setState
或dispatch
)来更新状态,并提供关于如何更新状态的具体信息。
现在让我们从高层次上探讨该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;
你能看出这两者有何关联吗?
请记住,上面的代码不应该在生产环境中使用。它是一个最小化版本的useReducer
钩子,用于帮助您将其与后端思维模型进行比较,但它缺少一些您将在本文中了解的重要内容。
现在(希望)您已经很好地了解了useReducer
高层次的工作方式,让我们进一步探讨细节。
减速机的工作原理
我们将首先处理减速器,因为它是主要逻辑发生的地方。
你可能已经从上面的例子中注意到,reducer 是一个接受两个参数的函数。第一个是 current state
,第二个是 the action
(在我们的后端类比中,它对应于 API 端点 + 请求可能包含的任何主体)。
请记住,您永远不必亲自向 Reducer 提供参数。这将由useReducer
Hook 自动处理:状态已知,并且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 };
switch
Reducer 通常由动作语句构成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 };
}
};
在此示例中,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>
如果我们想要一个按钮将计数器重置为 0,仍然使用我们的最后一个例子,你可以省略payload
:
<button onClick={() => dispatch({ type: 'reset' })}>
reset
</button>
需要注意的一点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);
我们以后会看到更多这样的例子。
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;
}
};
但我真的不喜欢这样。要么这个 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}`);
}
};
这样,您就不会错过拼写错误或忘记案例。
你应该在每一个行动中传播国家
到目前为止,我们只看到了一个非常简单的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;
该错误出现在 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;
这个例子实际上还可以进一步优化。你可能已经注意到,我们在 Reducer 中重复了一些代码: 和username
的email
情况本质上逻辑相同。对于两个字段来说,这还不算太糟,但我们可以有更多字段。
有一种方法可以重构代码,使所有输入仅执行一个操作,即使用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;
如您所见,我们现在只剩下一种操作类型: 。操作有效负载也发生了变化 - 它已成为具有(要更新的属性)和(要更新的值)textInput
的对象。key
value
key
如果你问我的话,这真是太棒了!
你可能会注意到,这段代码中还有一个地方重复了:onChange
事件处理程序。唯一改变的是payload.key
。
事实上,您可以进一步将其提取为可重复使用的操作,您只需提供即可key
。
我倾向于仅当减速器开始变得非常大时,或者当非常相似的动作被重复多次时才使用可重复使用的动作。
这是一种非常常见的模式,我们将在本文后面展示一个示例。
遵循常规动作结构
我所说的“常规动作结构”是我们迄今为止在本文中一直使用的结构:action
应该是一个具有必需type
和可选的对象文字payload
。
这是 Redux 构建 Action 的方式,也是最常用的方式。它已经过实践检验,是所有useReducer
Action 的默认设置。
这种结构的主要缺点是有时会有点冗长。但除非你非常习惯,否则useReducer
我建议你坚持使用 Redux 的方式。
糖语法:解构type
和payload
从动作
这关乎生活质量。与其在 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}`);
}
};
你甚至可以更进一步,解构状态。这只有在你的 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}`);
}
};
这就是所有的技巧和窍门!
useReducer
第三个参数:延迟初始化
值得一提的是,它useReducer
还有一个可选的第三个参数。该参数是一个函数,用于在需要时延迟初始化状态。
虽然不太常用,但真正需要的时候还是很有用的。React 文档里有一个很好的例子,演示了如何使用延迟初始化。
useState
vs useReducer
:何时使用哪个
现在你已经了解了它的useReducer
工作原理以及如何在组件中使用它,我们需要解决一个重要的问题。由于useState
和useReducer
是两种管理状态的方式,那么在什么情况下应该选择哪一种呢?
这类问题总是比较棘手,因为答案通常会根据提问对象而变化,而且高度依赖于具体情况。不过,还是有一些指导原则可以指导你做出选择。
首先,要知道useState
应该仍然是你管理 React 状态的默认选择。只有useReducer
当你在使用 时遇到问题useState
(并且可以通过切换到 解决useReducer
)时才切换到 。至少在你拥有足够的经验,useReducer
能够提前知道应该使用哪个之前,不要切换到 。
我将通过几个例子来说明何时使用useReducer
over 。useState
相互依赖的多个状态
一个很好的用例useReducer
是当您有多个相互依赖的状态时。
在创建表单时,这种情况很常见。假设你有一个文本输入框,并且想要跟踪三件事:
- 输入的值。
- 输入框是否已被用户“触摸”。这对于判断是否显示错误非常有用。例如,如果该字段是必填字段,则您希望在字段为空时显示错误。但是,如果用户之前从未访问过该输入框,则您不希望在首次渲染时显示错误。
- 是否有错误。
使用时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;
添加一些基本的 CSS 来设置error
类的样式,这样你就可以获得具有良好用户体验和简单逻辑的输入,这要归功于useReducer
:
.error {
border-color: red;
}
.error:focus {
outline-color: red;
}
管理复杂状态
另一个很好的用例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;
说真的,这段代码有多简洁明了?想象一下,如果用 4 个输入来做会怎么样useState
!好吧,这确实没那么糟糕,但它可以扩展到你想要的输入数量,除了输入本身,不需要添加任何其他东西。
您还可以在此基础上轻松构建。例如,我们可能希望将上一节的touched
anderror
属性添加到本节的四个输入中。
事实上,我建议你自己尝试一下,这是一个很好的练习,可以巩固你迄今为止所学到的知识!
那么如果这样做,用什么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;
如果您不熟悉该类型的语法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