useReducer
Reducer 可能是使用 React 的时候最常见到的一个词了。Reducer 通常用在处理复杂的数据逻辑并提供一个 State 的场景中。与useState
可以直接提供一个改变 State 的功能不同的是,useReducer
可以更加灵活的改变 State。
首先看一下useReducer
在组件中的使用格式:
const [state, dispatch] = useReducer(reducer, initialArg, init?);
在useReducer
的使用格式中,state
就是useReducer
经过运算处理最终返回的 State,dispatch
则是用来改变 State 的关键函数,对于调用了dispatch
以后的具体处理逻辑,则是在reducer
参数中定义的。结合后面 Flux、Redux 等章节的内容实际上就可以发现,useReducer
就是完成了一个单向数据流 State 的处理过程。
useReducer
的参数initialArg
表示用于初始化state
的值,而init
参数则是利用initialArg
参数计算state
初始值的函数。但是通常在使用中,只设定initialArg
就已经足够使用了。
reducer
函数的定义
useReducer
Hook 中的第一个参数reducer
是一个函数,它需要接受两个参数:state
和action
,reducer
函数的返回值则是更新以后的state
。
React 没有对useReducer
中reducer
函数的参数做任何限制,它们可以是任意合法的内容。例如以下利用switch
实现一个根据action
中携带的指令完成state
内容更新的reducer
函数。
function reducer(state, action) {
switch (action.type) {
case "increase":
return {
conter: state.counter + action.amount,
};
case "decrease":
return {
conter: state.counter - action.amount,
};
}
throw new Error("Unknown action: " + action.type);
}
在这个示例中可以看到,action
可以通过携带各种所需的内容,使reducer
函数可以对state
做出任何的改变。但是需要注意的是,reducer
函数所返回的state
,始终都应该是一个完整的state
对象结构,而不是只是发生了改变的state
不分结构,useReducer
Hook 不会对新旧state
自动进行拼合的。
虽然useReducer
不会对state
进行贼床上拼合,但是可以结合其他的工具库来完成state
对象的拼合操作,例如Ramda库提供的merge
系列函数,亦或者Javascript中提供的Object.assign()
方法。使用{ ...state }
的格式来复制已有state
中的属性也不失为一种好办法。
dispatch
函数
dispatch
函数是调用useReducer
以后返回的内容之一,也是在项目代码中调用reducer
函数更改state
值的唯一途径。dispatch
函数的唯一作用就是更新state
值并触发一次组件的重新渲染,它所接受的参数就是reducer
函数的action
参数内容。
从以上的描述实际上可以看出来,dispatch
函数就是把提供给它的action
参数转发给了reducer
函数,然后再触发一次组件的重新渲染。
例如结合前一节的示例,就可以这样实现一个组件。
function Amount() {
const [state, dispatch] = useReducer(reducer, { counter: 1 });
const handleIncrease = useCallback(() => dispatch({ type: "increase" }), []);
const handleDecrease = useCallback(() => dispatch({ type: "decrease" }), []);
return (
<div>
<div>
<span>Current Amount: {state.amount}</span>
<button onClick={handleDecrease}>Decrease</button>
<button onClick={handleIncrease}>Increase</button>
</div>
</div>
);
}
在使用dispatch
函数的时候,有以下几点是需要注意的:
dispatch
函数在调用以后不会立即更新state
,所以在调用dispatch
函数以后立刻读取state
的值,依旧还是更新之前的值,新的state
值只能在组件重新渲染之后才可以获得。- 如果新的
state
值与旧的state
值相同,也就是使用Object.is
方法进行比较获得的结果是true
,那么React就会跳过组件的重新渲染,这是一种非常重要的优化手段。 - 组件中的
state
并不是即时更新的,而是批量更新的。React会在所有的视见函数调用完毕以后对state
进行更新,这也是React为了减少组件的渲染次数做出的优化。
init
函数
init
函数在usereducer
的定义中不是必备的,在不提供init
函数的时候,useReducer
会默认使用其第二个参数initialArg
的值作为state
的初始值,但是如果state
的初始值是通过一定的逻辑处理生成的,那么就需要使用init
参数了。init
函数接受一个参数,就是useReducer
的第二个参数initialArg
,其返回值是state
的初始值。
可能有的读者可能觉得既然是通过initialArg
来生成state
的初始值,那么可以直接使用init(initialArg)
作为useReducer
的第二个参数,也就是如同下面示例中的样子。
function produceInitValue(initialArg) {
// 计算产生state初始值的逻辑
}
function RepeatInit() {
const [state, dispatch] = useReducer(reducer, produceInitValue(initialArg));
// 组件渲染逻辑
}
这个示例在实际运行的时候酒就会发现,produceInitValue(initialArg)
返回的值虽然仅在初次渲染的时候被使用了,但是在组件每一次渲染的时候都会重复执行一次,如果这个初始化过程比较费时,那么就会严重影响项目的性能。所以在这种情况下就体现出了useReducer
第三个参数的用途了。
useReducer
的第三个参数接受的是一个函数的名称,而不是函数的调用,它可以保证被指定的初始化函数仅在useReducer
首次使用的时候被调用一次,之后就不再运行。所以上面的示例可以优化成以下样子。
function produceInitValue(initialArg) {
// 计算产生state初始值的逻辑
}
function RepeatInit() {
// 注意这里只是使用了产生初始值的函数名,而不是调用。
const [state, dispatch] = useReducer(reducer, initialArg, produceInitValue);
// 组件渲染逻辑
}
一些常见的问题
reducer
和init
函数都执行了两次
在严格模式下,React 默认会调用两次reducer
和init
函数,这个特性主要是为了帮助开发者保持组件的纯粹。如果组件、reducer
函数和init
函数都是纯函数,那么 React 的这个特性并不会影响项目的逻辑。
例如以下代码中的reducer
函数就不是纯函数,在严格模式下就会向state.items
中增加两条数据。
function unpureReducer(state, action) {
switch (action.type) {
case "add_item":
state.items.push({
id: max(pluck("id", state.items)) + 1,
text: action.text,
});
return state;
// 以下其他分支代码省略
}
}
这种在reducer
函数中操作数组的方法正确的应该是每次都返回新的数组实例。例如可以修改成以下形式。
function unpureReducer(state, action) {
switch (action.type) {
case "add_item":
return {
...state,
items: [
...state.items,
{ id: max(pluck("id", state.items)) + 1, text: action.text },
],
};
// 以下其他分支代码省略
}
}
调用dispatch
以后出现了Too many re-renders
的错误
出现Toomany re-renders
的错误通常意味着组件渲染进入了死循环,也就是在徐建重新渲染过程中又发生了组件state
的改变,触发了新的重新渲染请求。在使用useReducer
的过程中出现这个错误,一般是在渲染期间再一次dispatch
了一个能够触发重新渲染的action
。大多数这样的错误都是存在于事件处理函数中。
例如以下组件中因为错误的书写了事件处理函数,就会造成dispatch
调用的死循环。
function FaultyReducer() {
const [state, dispatch] = useReducer(reducer, { count: 1 });
const handleClick = useCallback(() => dispatch({ type: "increase" }), []);
return (
<div>
<div>Current count: {state.count}</div>
<button onClick={handleClick()}>Increase</button>
</div>
);
}