简体   繁体   English

当 reducer 函数依赖于一个组件 prop 时,传递给 useReducer 钩子的 reducer 函数会针对一次调度调用执行多次

[英]Reducer function passed to useReducer hook is executed multiple times for one dispatch call when reducer function is dependant on a component prop

I was reading an article called “ A Complete Guide to useEffect ” and tried to implement and an example from “ Why useReducer Is the Cheat Mode of Hooks ” section.我正在阅读一篇名为“使用效果的完整指南”的文章,并尝试实现“ 为什么 useReducer 是 Hooks 的作弊模式”部分的一个示例。

In that example there is a Counter component that defines state (just a number) with the help of useReducer hook.在那个例子中,有一个Counter组件在useReducer钩子的帮助下定义了状态(只是一个数字)。 Reducer handles only one action — 'tick' on which it increments the state by the value of step prop. Reducer 只处理一个动作—— 'tick' ,它通过step prop 的值增加状态。 'tick' action is dispatched every second in interval function that is set up in useEffect hook once. 'tick'动作在useEffect钩子中设置的间隔函数中useEffect一次。
Here is the code from that example with some minor modifications:这是该示例中的代码,经过一些小的修改:

function Counter({ step }) {
    const [count, dispatch] = React.useReducer(reducer, 0);

    function reducer(state, action) {
        if (action.type === "tick") {
            console.log(`Reducer: state=${state} and step=${step}`);
            return state + step;
        } else {
            throw new Error(`Unknown action type: ${action.type}`);
        }
    }

    React.useEffect(() => {
        console.log("Create interval");
        const id = setInterval(() => {
            console.log("Dispatch");
            dispatch({ type: "tick" });
        }, 1000);
        return () => {
            console.log("Clear interval");
            clearInterval(id);
        };
    }, [dispatch]);

    return <h1>{count}</h1>;
}

function App() {
    const [step, setStep] = React.useState(0);

    return (
        <>
            <Counter step={step} />
            <input
                type="number"
                value={step}
                onChange={(e) => setStep(Number(e.target.value))}
            />
        </>
    );
}

What I found is that that example works on react@16.8.0-alpha.0 and doesn't on react@16.8.0 and higher.我发现该示例适用于react@16.8.0-alpha.0而不适用于react@16.8.0及更高版本。 When I run the code the initial value is 0 for both step and counter.当我运行代码时,步骤和计数器的初始值都是0 If I wait for 3 seconds without changing anything and then increment the step I get the following output:如果我等待 3 秒钟而不更改任何内容,然后增加步骤,我会得到以下输出:

Create interval
Dispatch
Reducer: state=0 and step=0
Dispatch
Reducer: state=0 and step=0
Dispatch
Reducer: state=0 and step=0
Reducer: state=0 and step=1
Reducer: state=1 and step=1
Reducer: state=2 and step=1
Dispatch
Reducer: state=3 and step=1
Reducer: state=3 and step=1
Dispatch
Reducer: state=4 and step=1
Dispatch
Reducer: state=5 and step=1

As you can see by the logs the reducer is executed more than the "tick" action was dispatched.正如您在日志中所看到的,reducer 执行的次数多于"tick"操作的调度次数。

I have managed to make it work as expected by creating a ref from step prop and memoizing the reducer with useCallback hook without any dependencies.我设法通过从step prop 创建 ref 并使用useCallback钩子useCallback reducer 来使其按预期工作,而没有任何依赖项。

const stepRef = React.useRef(step);
React.useEffect(() => {
  stepRef.current = step;
}, [step]);

const reducer = useCallback((state, action) => {
  if (action.type === "tick") {
    console.log(`Reducer: state=${state} and step=${stepRef.current}`);
    return state + stepRef.current;
  } else {
    throw new Error(`Unknown action type: ${action.type}`);
  }
}, []);

You can play with the examples here:您可以在此处使用示例:

But the questions still stand.但问题仍然存在。

  1. What of those behaviours ( react@16.8.0-alpha.0 or react@16.8.0 ) of useReducer hook from the buggy example is considered correct in nowadays React?有问题的例子中useReducer钩子的那些行为( react@16.8.0-alpha.0react@16.8.0 )在当今的 React 中被认为是正确的吗?
  2. Is it a bug?这是一个错误吗?
  3. If it's not a bug then why it works that way and reducer is triggered more than needed?如果它不是一个错误,那么为什么它会这样工作并且减速器被触发的次数超过了需要?

The answer to the last question should be somehow related to the the fact that reducer is being recreated.最后一个问题的答案应该与reducer 正在重新创建这一事实有关。 Be it on every render or even only when step prop changes it doesn't matter as memoizing the reducer with useCallback hook and passing [step] as a dependency array doesn't fix the problem.无论是在每次渲染中,还是仅在step prop 更改时,它都无关紧要,因为使用useCallback钩子useCallback reducer 并将[step]作为依赖项数组传递并不能解决问题。 Does anyone have any ideas about that?有没有人对此有任何想法?

Thanks!谢谢!

useReducer needs to remember reducer to be able to say whether a component needs a rerender (needs it to compute state and compare it with the previous one). useReducer需要记住reducer才能判断组件是否需要重新渲染(需要它计算状态并将其与前一个进行比较)。 But the reducer it has access to might be stale since you can "swap" the reducer.但是它可以访问的减速器可能已经过时,因为您可以“交换”减速器。 So, in the case when the reducer state is the same as the previous one, instead of throwing the result away and calling it a day React stashes the result as well as the reducer that was used to calculate it till the next re-render and then checks if the reducer is still the same.因此,在 reducer 状态与前一个相同的情况下,React 不会将结果扔掉并调用它,而是将结果以及用于计算它的 reducer 藏起来,直到下一次重新渲染和然后检查减速器是否仍然相同。 If not, the state is calculated again using the fresh reducer.如果不是,则使用新的减速器再次计算状态。

Your example is an edge case.你的例子是一个边缘情况。 It shouldn't work like that but Ract doesn't know when exactly it should throw away the stale reducer state updates - it always waits till the next rerender to compare reducers and compute the final state that will be used in re-render.它不应该那样工作,但 Ract 不知道它到底应该在什么时候丢弃陈旧的减速器状态更新——它总是等到下一次重新渲染来比较减速器并计算将在重新渲染中使用的最终状态。

Here's a simplified example that depicts it: Initially step is 0 .这是一个描述它的简化示例:最初step0 dispatch is run 3 times. dispatch运行 3 次。 The result is 0 + 0 + 0 = 0 and that's correctly displayed.结果是0 + 0 + 0 = 0并且显示正确。 Then, when you click on the button, step changes to 1 but dispatch isn't triggered even once.然后,当您单击按钮时, step更改为1但一次也不会触发dispatch Nevertheless, the result is now 3 because all previous actions were re-run with a newly created reducer.尽管如此,结果现在是3因为所有先前的操作都使用新创建的减速器重新运行。

 function App() { console.log("rendering"); const [step, setStep] = React.useState(0); const [count, dispatch] = React.useReducer(reducer, 0); function reducer(state, action) { if (action.type === "tick") { console.log(`Reducer: state=${state} and step=${step}`); return state + step; } } React.useEffect(() => { for(let i = 0; i < 3; i++) { console.log("Dispatch"); dispatch({ type: "tick" }); } }, []); return ( <div> <span>{count}&nbsp;&nbsp;</span> <button onClick={(e) => setStep(step + 1)} >step +1</button> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.development.js"></script> <div id="root"></div>

The solution is to make the reducer pure (as it should be from the beginning) and pass all the required data in action.payload ( ref ing things if needed).解决方案是使reducer 变得纯粹(从一开始就应该如此)并在action.payload传递所有必需的数据(如果需要,请ref内容)。

 function reducer(state, action) { if (action.type === "tick") { const { step } = action.payload; console.log(`Reducer: state=${state} and step=${step}`); return state + step; } } function Counter({ step }) { const [count, dispatch] = React.useReducer(reducer, 0); const stepRef = React.useRef(); stepRef.current = step; React.useEffect(() => { console.log("Create interval"); const id = setInterval(() => { console.log("Dispatch"); dispatch({ type: "tick", payload: { step: stepRef.current }}); }, 1000); return () => { console.log("Clear interval"); clearInterval(id); }; }, [dispatch]); return <span>{count}&nbsp;&nbsp;</span>; } function App() { const [step, setStep] = React.useState(0); return ( <div> <Counter step={step} /> <input type="number" value={step} onChange={(e) => setStep(Number(e.target.value))} /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.development.js"></script> <div id="root"></div>

import React from "react";
import ReactDOM from "react-dom";

function App() {
  const [step, setStep] = React.useState(0);

  function Counter({ step }) {
    const [count, dispatch] = React.useReducer(reducer, 0);

    function reducer(state, action) {
      if (action.type === "tick") {
        console.log(`Reducer: state=${state} and step=${step}`);
        return step === 0 ? state : state + step;
      } else {
        throw new Error(`Unknown action type: ${action.type}`);
      }
    }

    React.useEffect(() => {
      console.log("Create interval");
      const id = setInterval(() => {
        console.log("Dispatch");
        dispatch({ type: "tick" });
      }, 1000);
      return () => {
        console.log("Clear interval");
        clearInterval(id);
      };
    }, [dispatch]);

    return <h1>{count}</h1>;
  }

  return (
    <>
      <Counter step={step} />
      <input
        type="number"
        value={step}
        onChange={(e) => setStep(Number(e.target.value))}
      />
    </>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM