useReducer Action 分派两次

IT技术 reactjs react-hooks
2021-04-08 05:16:02

设想

我有一个返回动作的自定义钩子。父组件“Container”利用自定义钩子并将动作作为props传递给子组件。

问题

当动作从子组件执行时,实际分派发生两次。现在,如果孩子直接使用钩子并调用动作,则分派只发生一次。

如何重现它:

打开下面的沙箱并在 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 只执行了一次并且状态相应地改变。

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

5个回答

要首先澄清了现有的行为,该STATUS_FETCHING动作实际上只是被“派遣”(也就是说,如果你做console.log了正确的前dispatch呼叫getDatauseApiCall.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.jsuseApiCallReducer自定义钩子中定义了减速器这意味着在重新渲染时,您每次都会提供一个新的 reducer 函数,即使 reducer 代码是相同的。除非你的减速机需要使用传递到自定义钩的参数(而不是仅仅使用stateaction传递到减速参数),你应该定义在外部一级减速(即没有嵌套在其他函数内)。一般来说,我建议避免定义嵌套在另一个函数中的函数,除非它实际上使用了它嵌套范围内的变量。

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

编辑不同的减速器

@kstratis 我在答案的末尾添加了一个示例。
2021-05-25 05:16:02
2021-06-05 05:16:02
这是一个非常详细的答案和解决方案。谢谢瑞安!
2021-06-06 05:16:02
@RyanCogswell 太棒了!非常感谢您的帮助!最好的。
2021-06-08 05:16:02
你说“你的新减速器可能会产生不同的结果”......请说出一个减速器的结果会在下一次渲染中改变的场景......我的意思是为什么不重用“最后改变的结果”?
2021-06-09 05:16:02

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

非常感谢你,你救了我的命!
2021-06-20 05:16:02
哇,这太不可思议了......正是这样,StrictMode 导致减速器被调用两次......但我认为这是“不可能的”或其他一些问题,因为console.log减速器中的调用没有出现两倍...... .. 直到我读到这个reactjs.org/docs/... “从 React 17 开始,React 自动修改控制台方法,如 console.log() 以在第二次调用生命周期函数时使日志静音”(!!!!)见还有这个
2021-06-20 05:16:02

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

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

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

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

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

我删除了 <React.StrictMode>,但我遇到了同样的问题。
2021-06-21 05:16:02

正如 React 文档所说

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

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

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