useReducer操作分派两次

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)

编辑useAirportsData

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)

编辑不同的减速器

  • 那是一个非常详细的答案和解决方案。谢谢瑞安! (2认同)

Aja*_*mar 16

删除<React.StrictMode>将修复问题。

  • 哇,这太令人难以置信了...正是如此,StrictMode 导致减速器被调用两次...但我认为这是“不可能的”或其他一些问题,因为减速器中的 `console.log` 调用不是显示加倍......直到我读到此 https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects “从 React 17 开始,React 自动修改控制台方法,如 console.log () 在第二次调用生命周期函数时使日志静音”(!!!!)另请参阅[此](https://github.com/facebook/react/issues/20090#issuecomment-715926549) (2认同)

cdo*_*orn 12

如果您正在使用React.StrictMode,React 将使用相同的参数多次调用您的 reducer 以测试您的 reducer 的纯度。您可以禁用 StrictMode,以测试您的减速器是否正确记忆。

来自https://github.com/facebook/react/issues/16295#issuecomment-610098654

没有问题”。React 故意调用您的减速器两次,以使任何意外的副作用更加明显。由于您的 reducer 是纯的,调用它两次不会影响您的应用程序的逻辑。所以你不应该担心这个。

在生产中,它只会被调用一次。


Oma*_*nab 7

通过删除<React.StrictMode>调度不会被多次调用,我也面临这个问题,这个解决方案有效


Mar*_*tty 5

正如 React 文档所说

严格模式无法自动为您检测副作用,但它可以通过使它们更具确定性来帮助您发现它们。这是通过有意重复调用以下函数来完成的: [...] 传递给 useState、useMemo 或 useReducer 的函数

这是因为 reducer 必须是纯的,它们每次都必须使用相同的参数提供相同的输出,并且 React Strict Mode 会通过调用您的 reducer 两次来自动测试(有时)。

这应该不是问题,因为这是一种仅限于开发的行为,它不会出现在生产中,所以我不建议退出,<React.StrictMode>因为它可以非常有助于突出与键、副作用等相关的许多问题。

  • 我遇到了这个问题,但这个答案对我来说似乎没有意义。状态被更新,因此减速器的输入实际上已经改变。第二个条目的状态不同。比如说,你有一个开关;运行它两次会将其恢复到原始状态...我们是说减速器不应该使用旧状态的部分来生成新状态吗? (2认同)