useReducer 动作分派两次

useReducer Action dispatched twice

场景

我有一个自定义挂钩 returns 一个动作。 parent 组件 "Container" 使用自定义挂钩并将操作作为 prop 传递给 children 组件。

问题

当从 child 组件执行操作时,实际调度发生两次。 现在,如果 children 直接利用挂钩并调用操作,则调度只会发生一次。

如何重现:

打开下面的沙盒并打开 chrome 上的开发工具,这样您就可以看到我添加的控制台日志。

https://codesandbox.io/s/j299ww3lo5?fontsize=14

Main.js(children 组件)你会看到我们调用了 props.actions.getData()

在 DevTools 上,清除日志。 在预览中,在表单上输入任何值并单击按钮。 在控制台日志中,您将看到类似 redux-logger 的操作,并且您会注意到 STATUS_FETCHING 操作执行了两次而状态没有改变。

现在转到 Main.js 并注释掉第 9 行并取消注释第 10 行。我们现在基本上直接使用自定义挂钩。

在 DevTools 上,清除日志。 在预览中,在表单上输入任何值并单击按钮。 在控制台日志上,现在您将看到 STATUS_FETCHING 只执行了一次并且状态相应地发生了变化。

虽然没有明显的性能损失,但我不明白为什么会这样。我可能太专注于 Hooks 而错过了一些如此愚蠢的东西......请让我摆脱这个谜题。谢谢!

首先要澄清现有行为,STATUS_FETCHING 操作实际上只是 "dispatched"(即,如果您在 dispatch 调用之前执行 console.log getData within useApiCall.js) once, but the reducer code was executing twice.

我可能不知道要寻找什么来解释为什么在写这个有点相关的答案时没有我的研究:.

您会在该答案中找到以下来自 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;
  }

特别注意注释,指出 React 可能需要重做一些工作如果 reducer 已更改。问题是,在您的 useApiCallReducer.js 中,您在 useApiCallReducer 自定义挂钩中定义了减速器。这意味着在重新渲染时,您每次都提供一个新的 reducer 函数,即使 reducer 代码是相同的。除非你的 reducer 需要使用传递给自定义钩子的参数(而不是仅仅使用传递给 reducer 的 stateaction 参数),你应该在外层定义 reducer(即不嵌套在里面)另一个功能)。一般来说,我建议避免定义嵌套在另一个函数中的函数,除非它实际上使用嵌套在其中的作用域中的变量。

当 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 或其他状态上)时,这里有一个关于这个问题的变体的相关答案:

下面是一个非常人为的示例,用于演示渲染过程中减速器的更改需要重新执行的场景。您可以在控制台中看到,第一次通过其中一个按钮触发减速器时,它会执行两次——一次使用初始减速器 (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);

删除 <React.StrictMode> 将解决问题。

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

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

There is no "problem". React intentionally calls your reducer twice to make any unexpected side effects more apparent. Since your reducer is pure, calling it twice doesn't affect the logic of your application. So you shouldn't worry about this.

In production, it will only be called once.

通过删除 <React.StrictMode> dispatch 将不会被多次调用,我也遇到了这个问题并且这个解决方案有效

As React docs says:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions: [...] Functions passed to useState, useMemo, or useReducer

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

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