原文:
Getting to Know the useReducer React Hook作者:Kingsley Silas
译者:博轩
useReducer
是 React 16.8.0 中为数不多由官方提供的 React Hook 之一。它接受一个 reducer 函数 ,以及一个初始的应用程序状态,然后返回当前应用程序的状态,和一个调度函数(dispatch)。
一个简单的例子:
const [state, dispatch] = useReducer(reducer, initialState);
这样有什么好处?一个好主意是让我们试着想象一下,一个应用初次加载属性时的所有情况。它可能是可交互式的地图上的一个起点。或许是允许用户使用一个默认的模型来自定义选项,构建一个自定义汽车。这里有一个非常简洁的计算器,当计算器重置时,使用 useReducer
来使应用程序恢复默认状态。
https://codepen.io/dpgian/emb…
我们将在这篇文章中深入研究几个例子,了解一下 useReducer Hook
本身,以及应该何时使用。
全能的 reducer
说起 useState
,就不得不提及 JavaScript 的 reduce
方法。最开始,我们很难将它们联系起来,但是 Sarah 的一篇关于 reducer
的文章 可以帮助我们更好的理解。
关于
reducer
最重要的一点就是:
它每次只返回一个值。
reducer
的工作就是减少。那个值可以是数字,字符串,对象,数组或者对象,但是它总是一个值。
reducer
在很多情况下都很有效,但是他对于处理输入一组值,返回一个值的情况非常有用。
假设我们有一个数字数组,reduce
将依次累加每一个值。这是数组:
const numbers = [1, 2, 3]
…以及一个函数,每次 reducer
中的计算都会在控制台打印出来。这有助于我们理解 reducer
将数组提取为单个数字的过程。
const reducer = function (tally, number) {
console.log(`Tally: ${tally}, Next number: ${number}, New Total: ${tally + number}`)
return tally + number
}
现在,让我们运行一个 reducer
。正如我们所看到的,reduce
接收一个调度函数,以及一个初始状态。让我们传入一个 reducer
函数,以及一个初始值:0。
const total = numbers.reduce(reducer, 0)
这是控制台打印的内容:
"Tally: 0, Next number: 1, New Total: 1"
"Tally: 1, Next number: 2, New Total: 3"
"Tally: 3, Next number: 3, New Total: 6"
看 reduce
是如何将一个初始值累加,得到我们的最终结果的。在这个例子中,最终结果是 6。
我也十分喜欢 Dave Ceddia
的示例 ,他展示了如何使用 reduce
来拼写一个单词:
var letters = ['r', 'e', 'd', 'u', 'c', 'e'];
// `reduce` takes 2 arguments:
// - a function to do the reducing (you might say, a "reducer")
// - an initial value for accumulatedResult
var word = letters.reduce(
function(accumulatedResult, arrayItem) {
return accumulatedResult + arrayItem;
},
''); // <-- notice this empty string argument: it's the initial value
console.log(word) // => "reduce"
组合使用 useReducer ,state ,action
好的,接下来到了这篇文章的重点: useReducer
。到了这里的一切都很重要,因为使用 reduce
调用一个函数来处理初始值的方式,就是我们接下来的目标。它是同一种概念,但是会返回一个数组包含两个元素,当前的状态和调度函数。
const [state, dispatch] = useReducer(reducer, initialArg, init);
第三个参数
init
是什么?它是一个可选值,可以用来惰性提供初始状态。这意味着我们可以使用使用一个
init
函数来计算初始状态/值,而不是显式的提供值。如果初始值可能会不一样,这会很方便,最后会用计算的值来代替初始值。
为了使它工作,我们需要做一些事情:
- 定义初始状态
- 提供一个包含
action
的函数来更新state
- 触发
useReducer
,基于初始值计算并更新state
。
计数器就是一个经典的例子。事实上这也是官方文档使用这个例子的原因:
https://codepen.io/kinsomicro…
这是一个很好的例子,因为它演示了每次通过单击增加或减少按钮触发操作时如何使用初始状态(零值)来计算新值。我们甚至可以在其中输入一个“重置”按钮,将总数恢复到初始状态零。
示例:汽车定制器
https://codepen.io/geoffgraha…
在此示例中,我们假设用户已经选择了自己要购买的汽车。但是,我们希望用户可以为汽车添加额外的选项。每个选项的价格都会影响汽车的总价。
首先,我们需要创建初始状态,其中包括汽车,可以跟踪功能的空数组 features
,$26,395 的起始价格 price
,一个存放未选配件的列表 store
,用户可以选择他们想要的东西。
const initialState = {
additionalPrice: 0,
car: {
price: 26395,
name: "2019 Ford Mustang",
image: "https://cdn.motor1.com/images/mgl/0AN2V/s1/2019-ford-mustang-bullitt.jpg",
features: []
},
store: [
{ id: 1, name: "V-6 engine", price: 1500 },
{ id: 2, name: "Racing detail package", price: 1500 },
{ id: 3, name: "Premium sound system", price: 500 },
{ id: 4, name: "Rear spoiler", price: 250 }
]
};
我们的 reducer
功能将处理两件事:添加和删除新项目。
const reducer = (state, action) => {
switch (action.type) {
case "REMOVE_ITEM":
return {
...state,
additionalPrice: state.additionalPrice - action.item.price,
car: { ...state.car, features: state.car.features.filter((x) => x.id !== action.item.id)},
store: [...state.store, action.item]
};
case "BUY_ITEM":
return {
...state,
additionalPrice: state.additionalPrice + action.item.price,
car: { ...state.car, features: [...state.car.features, action.item] },
store: state.store.filter((x) => x.id !== action.item.id)
}
default:
return state;
}
}
当用户选择他想要的项目时,我们更新汽车的 features
,增加 additionalPrice
并从商店中删除该项目。我们确保 state
其他部分会保持原样。当用户从功能列表中删除项目时,我们会执行类似操作 – 减少额外价格,将项目返回到商店。
以下是App组件的代码。
const App = () => {
const inputRef = useRef();
const [state, dispatch] = useReducer(reducer, initialState);
const removeFeature = (item) => {
dispatch({ type: 'REMOVE_ITEM', item });
}
const buyItem = (item) => {
dispatch({ type: 'BUY_ITEM', item })
}
return (
<div>
<div className="box">
<figure className="image is-128x128">
<img src={state.car.image} />
</figure>
<h2>{state.car.name}</h2>
<p>Amount: ${state.car.price}</p>
<div className="content">
<h6>Extra items you bought:</h6>
{state.car.features.length ?
(
<ol type="1">
{state.car.features.map((item) => (
<li key={item.id}>
<button
onClick={() => removeFeature(item)}
className="button">X
</button>
{item.name}
</li>
))}
</ol>
) : <p>You can purchase items from the store.</p>
}
</div>
</div>
<div className="box">
<div className="content">
<h4>Store:</h4>
{state.store.length ?
(
<ol type="1">
{state.store.map((item) => (
<li key={item.id}>\
<button
onClick={() => buyItem(item)}
className="button">Buy
</button>
{item.name}
</li>
))}
</ol>
) : <p>No features</p>
}
</div>
<div className="content">
<h4>
Total Amount: ${state.car.price + state.additionalPrice}
</h4>
</div>
</div>
</div>
);
}
调度操作会包含所选项的详细信息。我们使用 action
的类型来确定 reducer
函数如何处理状态的更新。您可以看到渲染视图会根据您的操作而做出改变 – 从商店购买的商品会从商店中删除,并添加到功能列表当中。此外,总金额也会更新。毫无疑问,我们可以对示例进行修改达到学习的目的。
我们可以使用 useState 来代替吗?
聪明的读者可能一直在想这个问题。我的意思是,setState
大致会做相同的事情,不是吗?返回一个具备状态的值,以及一个可以使用新值重新渲染组件的函数。
const [state, setState] = useState(initialState);
我们甚至可以使用 useState
来实现官方文档中的计数器的例子。但是 useReducer
在处理复杂状态的时候是最优解。Kent C. Dodds 写了一个两者之间的差异(虽然他经常使用 setState
)他提供了一个 useReducer
的最佳实践:
当你一个元素中的状态,依赖另一个元素中的状态,最好使用
useReducer
例如,你正在完成一个井字游戏。你的组件中的 状态被称为
squares
,它包含了左右方格,以及其中的值。
我的经验是使用 useReducer
来处理复杂的状态,尤其是初始状态是基于其他元素生成的情况下。
等等,我们已经有 Redux 了!
如果你使用 Redux 工作,也会理解这里所涉及的所有内容,因为它的设计理念是通过 Context API 来存储,传递组件之间的状态 – 不必通过其他组件传递 props
来实现。
那么, useReducer
取代 Redux 了吗?不,我的意思是,你基本可以通过 useContext hook
来实现你自己的 Redux,但是,这并不代表 Redux 没有用了,它仍然有许多其他的功能和优点值得考虑。
你会在哪里使用 useReducer
?他是否有比 setState
更好的地方?也许你可以尝试使用我们这里介绍的想法来构建一些东西,下面是一些想法。
- 一个日历,会展示今天的日期,但允许用户选择其他日期。还可以添加一个“今天”按钮,帮助用户返回到今天的日期。
- 您可以尝试改进汽车示例 – 让用户拥有一个购物车列表。你可以为它定义初始状态,然后用户可以添加他们想要的额外功能,并收取一定费用。这些功能可以是预定义的,也可以由用户自定义。
本文已经联系原文作者,并授权翻译,转载请保留原文链接