ses*_*eva 12 reactjs react-hooks
我有一个返回操作的自定义钩子。父组件“容器”利用自定义钩子并将动作作为道具传递给子组件。
当从子组件执行该操作时,实际的分派发生两次。现在,如果子级直接利用该钩子并调用了该动作,则调度仅发生一次。
如何复制它:
打开下面的沙箱,然后在chrome上打开devtools,这样您就可以看到我添加的控制台日志。
https://codesandbox.io/s/j299ww3lo5?fontsize=14
Main.js(儿童组件),您将看到我们调用props.actions.getData()
在DevTools上,清除日志。在“预览”上,在表单上输入任何值,然后单击按钮。在控制台日志上,您将看到类似redux-logger的操作,并且您将注意到STATUS_FETCHING操作被执行了两次而没有更改状态。
现在转到Main.js并注释掉第9行和第10行的注释。我们现在基本上是在直接使用自定义钩子。
在DevTools上,清除日志。在“预览”上,在表单上输入任何值,然后单击按钮。现在,在控制台日志上,您将看到STATUS_FETCHING仅执行一次,并且状态会相应更改。
尽管没有明显的性能损失,但我不明白为什么会这样。我可能过于关注挂钩,而我却错过了一些愚蠢的事情……请把我从这个难题中解脱出来。谢谢!
Rya*_*ell 17
To first clarify the existing behavior, the STATUS_FETCHING action was actually only being "dispatched" (i.e. if you do a console.log
right before the dispatch
call in getData
within useApiCall.js
) once, but the reducer code was executing twice.
I probably wouldn't have known what to look for to explain why if it hadn't been for my research when writing this somewhat-related answer: React hook rendering an extra time.
You'll find the following block of code from React shown in that answer:
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;
}
Run Code Online (Sandbox Code Playgroud)
In particular, notice the comments indicating React may have to redo some of the work if the reducer has changed. The issue is that in your useApiCallReducer.js
you were defining your reducer inside of your useApiCallReducer
custom hook. This means that on a re-render, you provide a new reducer function each time even though the reducer code is identical. Unless your reducer needs to use arguments passed to the custom hook (rather than just using the state
and action
arguments passed to the reducer), you should define the reducer at the outer level (i.e. not nested inside another function). In general, I would recommend avoiding defining a function nested within another unless it actually uses variables from the scope it is nested within.
When React sees the new reducer after the re-render, it has to throw out some of the work it did earlier when trying to determine whether a re-render would be necessary because your new reducer might produce a different result. This is all just part of performance optimization details in the React code that you mostly don't need to worry about, but it is worth being aware that if you redefine functions unnecessarily, you may end up defeating some performance optimizations.
To solve this I changed the following:
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);
}
Run Code Online (Sandbox Code Playgroud)
to instead be:
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);
}
Run Code Online (Sandbox Code Playgroud)
Here's a related answer for a variation on this problem when the reducer has dependencies (e.g. on props or other state) that require it to be defined within another function: React useReducer Hook fires twice / how to pass props to reducer?
Below is a very contrived example to demonstrate a scenario where a change in the reducer during render requires it to be re-executed. You can see in the console, that the first time you trigger the reducer via one of the buttons, it executes twice -- once with the initial reducer (addSubtractReducer) and then again with the different reducer (multiplyDivideReducer). Subsequent dispatches seem to trigger the re-render unconditionally without first executing the reducer, so only the correct reducer is executed. You can see particularly interesting behavior in the logs if you first dispatch the "nochange" action.
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);
Run Code Online (Sandbox Code Playgroud)
Aja*_*mar 16
删除<React.StrictMode>
将修复问题。
cdo*_*orn 12
如果您正在使用React.StrictMode
,React 将使用相同的参数多次调用您的 reducer 以测试您的 reducer 的纯度。您可以禁用 StrictMode,以测试您的减速器是否正确记忆。
来自https://github.com/facebook/react/issues/16295#issuecomment-610098654:
没有问题”。React 故意调用您的减速器两次,以使任何意外的副作用更加明显。由于您的 reducer 是纯的,调用它两次不会影响您的应用程序的逻辑。所以你不应该担心这个。
在生产中,它只会被调用一次。
严格模式无法自动为您检测副作用,但它可以通过使它们更具确定性来帮助您发现它们。这是通过有意重复调用以下函数来完成的: [...] 传递给 useState、useMemo 或 useReducer 的函数
这是因为 reducer 必须是纯的,它们每次都必须使用相同的参数提供相同的输出,并且 React Strict Mode 会通过调用您的 reducer 两次来自动测试(有时)。
这应该不是问题,因为这是一种仅限于开发的行为,它不会出现在生产中,所以我不建议退出,<React.StrictMode>
因为它可以非常有助于突出与键、副作用等相关的许多问题。
归档时间: |
|
查看次数: |
1643 次 |
最近记录: |