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>
);
}