共计 4663 个字符,预计需要花费 12 分钟才能阅读完成。
为了保证的可读性,本文采用意译而非直译。
想阅读更多优质文章请猛戳 GitHub 博客, 一年百来篇优质文章等着你!
这个礼拜《大迁世界》有抽奖活动,奖品:专栏《左耳听风》x3, 技术书 x5, 欢迎关注回复:抽奖
看到 “reducer”
这个词,容易让人联想到 Redux,但是在本文中,不必先理解Redux
才能阅读这篇文章。咱们将一起讨论“reducer
”实际上是什么,以及如何利用 useReducer
来管理组件中的复杂状态,以及这个新钩子对 Redux 意味着什么?
Reducer 是什么鬼
如果你熟悉 Redux 或数组上中的 reduce
方法,你大概就知道“reducer”是什么。如果不熟悉,“reducer
”大概是一个带有 2
个值并返回 1
个值的函数这么个意思。
如果你有一系列的东西,并且想将这些东西组合成一个单独的物体。“函数式编程”中就是使用 Array 的reduce
函数。例如,如果你有一个数字数组并且想得到数组中所有数字的总和,咱们就可以写一个 reducer
函数并将它传递给reduce
,如下所示:
let numbers = [1, 2, 3];
let sum = numbers.reduce((total, number) => {return total + number;},0)
如果你以前没用过这个,它可能看起来有点神秘。它所做的是为数组的每个元素调用函数,传入前一个 total
和当前元素 number
。无论你返回什么,都会成为新的 total
。reduce
的第二个参数 (在本例中为0
) 是total
的初始值。在本例中,reduce
函数会被调用 3
次:
- 调用
(0, 1)
返回1
- 调用
(1, 2)
返回3
- 调用
(3, 4)
返回6
reduce
返回 6
,它保存在sum
中。
使用 useReducer
又会是什么样的?
各位花了在半篇幅来解释 Array 的reduce
函数,因为 useReducer
参数与 reduce
相同,并且工作方式基本一样。useReducer
接收 (state, action) => newState
,并且返回了一个与当前 state
成对的 dispatch
的方法。咱们使用 useReducer
来编写上面的求和例子。
useReducer((state, acton) => {return state + action}, 0)
useReducer
返回一个包含 2
个元素的数组,类似于 useState
hook。第一个是当前状态,第二个是dispatch
方法,如下所示:
const [sum, dispatch] = useReducer((state, action) => {return state + action}, 0)
注意 ,state
可以是任何值,它不一定是一个对象,可以是一个数字,一个数组,或者其他任何类型。
尽管 useReducer
是扩展的 hook,而 useState
是基本的 hook,但 useState
实际上执行的也是一个 useReducer
。这意味着 useReducer
是更原生的,你能在任何使用 useState
的地方都替换成使用 useReducer
。
import React, {useReducer} from 'react';
function Counter() {
// First render will create the state, and it will
// persist through future renders
const [sum, dispatch] = useReducer((state, action) => {return state + action;}, 0);
return (
<>
{sum}
<button onClick={() => dispatch(1)}>
Add 1
</button>
</>
);
}
点击按钮 dispatch
一个值为 1
的action
,该 action
将被添加到当前状态,然后组件使用新的状态重新渲染。
这里故意展示了,派发 action
没有遵循 Redux 的典型模式 {type: "INCREMENT BY"、value: 1}
或其他类似的东西。hook 的世界是一个新的世界: 值得考虑的是,你是否发现旧的模式有价值并希望保留它们,或者你是否愿意更改它们。
一些更复杂的例子
再来看看更接近典型Redux reducer
的例子。创建一个组件来管理购物列表,这里看还会使用另外一个 hook:useRef
。
首先,导入两个 hook
import React, {useReducer, useRef} from 'react';
然后创建一个设置 ref
和reducer
的组件。ref
保存对表单的引用,以便咱们可以提取其值。
function ShoppingList() {const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {switch (action.type) {// do something with the action}
}, []);
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (<li key={item.id}>
{item.name}
</li>
))}
</ul>
</>
);
}
请注意,在这种情况下,咱们的“state
”是一个数组。咱们通过 useReducer
第二个参数将它初始化为一个空数组,并从 reducer
函数返回一个数组。
useRef Hook
useRef
hook 为 DOM
节点创建持久引用。调用 useRef
会创建一个空的节点。它返回的对象具有 current
属性,因此在上面的示例中,咱们可以使用 inputRef.current
访问输入的 DOM 节点。如果你熟悉React.createRef()
,则其工作原理非常相似。
但是,useRef
返回的对象不仅仅是一种保存 DOM 引用的方法。它可以保存特定于此组件实例的任何值,并且它在渲染之间保持不变。
useRef
可用于创建通用实例变量,就像使用具有 this.whatever = value
的React
类组件一样。唯一的问题是,写入它会被视为“副作用”,因此咱们无法在渲染过程中更改它,需要在useEffect
hook 中才能修改。
回到 useReducer
示例
我们用表单来处理用户的输入,按回车提交表彰。现在来编写 handleSubmit
函数,该函数主要做的是将一个项添加到列表中,以及处理 reducer
中的 action
。
function ShoppingList() {const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {switch (action.type) {
case 'add':
return [
...state,
{
id: state.length,
name: action.name
}
];
default:
return state;
}
}, []);
function handleSubmit(e) {e.preventDefault();
dispatch({
type: 'add',
name: inputRef.current.value
});
inputRef.current.value = '';
}
return (// ... same ...);
}
在 reducer
函数中主要判断两种情况:一种用于 action.type==='add'
的情况,还有就是默认下的情况。
当 action.type
为 add
时,它返回一个包含所有旧元素的新数组,以及最后的新元素。
这里有一点需要注意的是,咱们使用数组的 length
作为一种自动递增的 ID 方便演示,但是对于一个真正的应用程序来说这是不可靠,因为它可能导致重复的 ID 和 bug。(最好使用 uuid 这样的库,或者让服务器生成一个惟一的 ID!)
当用户在输入框中按 Enter 键时会调用 handleSubmit
函数,因此咱们需要调用 preventDefault
以避免在发生这种情况时重新加载整页。然后 dispatch
派发一个 action
。
删除项
现在来看看如何从列表中删除项的。
在项目中添加一个删除 <button>
,点击该按钮派发 它将发送一个 action type === "remove"
的操作,以及要删除的项的索引。
然后咱们只需要在 reducer
中处理该action
function ShoppingList() {const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {switch (action.type) {
case 'add':
// ... same as before ...
case 'remove':
// keep every item except the one we want to remove
return state.filter((_, index) => index != action.index);
default:
return state;
}
}, []);
function handleSubmit(e) {/*...*/}
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (<li key={item.id}>
{item.name}
<button
onClick={() => dispatch({ type: 'remove', index})}
>
X
</button>
</li>
))}
</ul>
</>
);
}
练习: 清空列表
试着添加一个功能:添加一个清空列表的按钮。
在 <ul>
上方插入一个按钮,并为其提供一个onClick prop
,派发一个action
,type
为“clear
”的动作,并在 reducer
方法执行清空列表的动作。
可以在前面 CodeSandbox 的基础上完成。
Redux 会死吗
大部分人看到 useReducer
hook, React 现在已经内置了reducer
,它有Context
传递数据,所以可能会想到 Redux
会不会因此就死了,以下是原作者给出的一些看法。
作者不认为 useReducer
会杀死 Redux
。React Hook 扩展了React
在状态管理方面的能力,所以会让使用 Redux
的情况减少。
Redux
仍然比 Context + useReducer
的组合做得更多,它具有 Redux DevTools 用于调试,可定制中间件、,以及整个相关库生态系统,当然 Redu x 在很多地方都被过度使用了,但它仍然具有强大的功能。
Redux
提供了一个全局存储,可以在其中集中保存应用程序数据。useReducer
本地化到特定组件。但是,没有什么可以阻止咱们使用 useReducer
和useContext
构建自己的迷你redux
。如果你想这么做,而且符合你的需要,那就去做吧!
代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
交流
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq44924588…
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复 福利,即可看到福利,你懂的。