要首先澄清了现有的行为,该STATUS_FETCHING动作实际上只是被“派遣”(也就是说,如果你做console.log
了正确的前dispatch
呼叫getData
内useApiCall.js
)一次,但减速机代码被执行两次。
如果在编写这个有点相关的答案时没有进行研究,我可能不知道要寻找什么来解释原因:React hook rendering a extra time。
您会在该答案中找到以下来自 React 的代码块:
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
特别要注意的是,如果reducer 发生了变化, React 可能需要重做一些工作的注释。问题在于,您useApiCallReducer.js
在useApiCallReducer
自定义钩子中定义了减速器。这意味着在重新渲染时,您每次都会提供一个新的 reducer 函数,即使 reducer 代码是相同的。除非你的减速机需要使用传递到自定义钩的参数(而不是仅仅使用state
并action
传递到减速参数),你应该定义在外部一级减速(即没有嵌套在其他函数内)。一般来说,我建议避免定义嵌套在另一个函数中的函数,除非它实际上使用了它嵌套范围内的变量。
当 React 在重新渲染后看到新的 reducer 时,它必须放弃之前在尝试确定是否需要重新渲染时所做的一些工作,因为您的新 reducer 可能会产生不同的结果。这只是 React 代码中性能优化细节的一部分,你通常不需要担心,但值得注意的是,如果你不必要地重新定义函数,你最终可能会失败一些性能优化。
为了解决这个问题,我更改了以下内容:
import { useReducer } from "react";
import types from "./types";
const initialState = {
data: [],
error: [],
status: types.STATUS_IDLE
};
export function useApiCallReducer() {
function reducer(state, action) {
console.log("prevState: ", state);
console.log("action: ", action);
switch (action.type) {
case types.STATUS_FETCHING:
return {
...state,
status: types.STATUS_FETCHING
};
case types.STATUS_FETCH_SUCCESS:
return {
...state,
error: [],
data: action.data,
status: types.STATUS_FETCH_SUCCESS
};
case types.STATUS_FETCH_FAILURE:
return {
...state,
error: action.error,
status: types.STATUS_FETCH_FAILURE
};
default:
return state;
}
}
return useReducer(reducer, initialState);
}
改为:
import { useReducer } from "react";
import types from "./types";
const initialState = {
data: [],
error: [],
status: types.STATUS_IDLE
};
function reducer(state, action) {
console.log("prevState: ", state);
console.log("action: ", action);
switch (action.type) {
case types.STATUS_FETCHING:
return {
...state,
status: types.STATUS_FETCHING
};
case types.STATUS_FETCH_SUCCESS:
return {
...state,
error: [],
data: action.data,
status: types.STATUS_FETCH_SUCCESS
};
case types.STATUS_FETCH_FAILURE:
return {
...state,
error: action.error,
status: types.STATUS_FETCH_FAILURE
};
default:
return state;
}
}
export function useApiCallReducer() {
return useReducer(reducer, initialState);
}
当 reducer 具有需要在另一个函数中定义它的依赖项(例如,在 props 或其他状态上)时,这是针对此问题的变体的相关答案:React useReducer Hook 触发两次/如何将 props 传递给 reducer?
下面是一个非常人为的示例,用于演示渲染过程中 reducer 的更改需要重新执行的场景。您可以在控制台中看到,第一次通过其中一个按钮触发减速器时,它执行两次——一次使用初始减速器 (addSubtractReducer),然后再次使用不同的减速器 (multiplyDivideReducer)。后续分派似乎会无条件地触发重新渲染,而无需先执行减速器,因此只会执行正确的减速器。如果您首先调度“nochange”操作,您可以在日志中看到特别有趣的行为。
import React from "react";
import ReactDOM from "react-dom";
const addSubtractReducer = (state, { type }) => {
let newState = state;
switch (type) {
case "increase":
newState = state + 10;
break;
case "decrease":
newState = state - 10;
break;
default:
newState = state;
}
console.log("add/subtract", type, newState);
return newState;
};
const multiplyDivideReducer = (state, { type }) => {
let newState = state;
switch (type) {
case "increase":
newState = state * 10;
break;
case "decrease":
newState = state / 10;
break;
default:
newState = state;
}
console.log("multiply/divide", type, newState);
return newState;
};
function App() {
const reducerIndexRef = React.useRef(0);
React.useEffect(() => {
reducerIndexRef.current += 1;
});
const reducer =
reducerIndexRef.current % 2 === 0
? addSubtractReducer
: multiplyDivideReducer;
const [reducerValue, dispatch] = React.useReducer(reducer, 10);
return (
<div>
Reducer Value: {reducerValue}
<div>
<button onClick={() => dispatch({ type: "increase" })}>Increase</button>
<button onClick={() => dispatch({ type: "decrease" })}>Decrease</button>
<button onClick={() => dispatch({ type: "nochange" })}>
Dispatch With No Change
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);