簡體   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

我正在閱讀一篇名為“使用效果的完整指南”的文章,並嘗試實現“ 為什么 useReducer 是 Hooks 的作弊模式”部分的一個示例。

在那個例子中,有一個Counter組件在useReducer鈎子的幫助下定義了狀態(只是一個數字)。 Reducer 只處理一個動作—— 'tick' ,它通過step prop 的值增加狀態。 'tick'動作在useEffect鈎子中設置的間隔函數中useEffect一次。
這是該示例中的代碼,經過一些小的修改:

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))}
            />
        </>
    );
}

我發現該示例適用於react@16.8.0-alpha.0而不適用於react@16.8.0及更高版本。 當我運行代碼時,步驟和計數器的初始值都是0 如果我等待 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

正如您在日志中所看到的,reducer 執行的次數多於"tick"操作的調度次數。

我設法通過從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}`);
  }
}, []);

您可以在此處使用示例:

但問題仍然存在。

  1. 有問題的例子中useReducer鈎子的那些行為( react@16.8.0-alpha.0react@16.8.0 )在當今的 React 中被認為是正確的嗎?
  2. 這是一個錯誤嗎?
  3. 如果它不是一個錯誤,那么為什么它會這樣工作並且減速器被觸發的次數超過了需要?

最后一個問題的答案應該與reducer 正在重新創建這一事實有關。 無論是在每次渲染中,還是僅在step prop 更改時,它都無關緊要,因為使用useCallback鈎子useCallback reducer 並將[step]作為依賴項數組傳遞並不能解決問題。 有沒有人對此有任何想法?

謝謝!

useReducer需要記住reducer才能判斷組件是否需要重新渲染(需要它計算狀態並將其與前一個進行比較)。 但是它可以訪問的減速器可能已經過時,因為您可以“交換”減速器。 因此,在 reducer 狀態與前一個相同的情況下,React 不會將結果扔掉並調用它,而是將結果以及用於計算它的 reducer 藏起來,直到下一次重新渲染和然后檢查減速器是否仍然相同。 如果不是,則使用新的減速器再次計算狀態。

你的例子是一個邊緣情況。 它不應該那樣工作,但 Ract 不知道它到底應該在什么時候丟棄陳舊的減速器狀態更新——它總是等到下一次重新渲染來比較減速器並計算將在重新渲染中使用的最終狀態。

這是一個描述它的簡化示例:最初step0 dispatch運行 3 次。 結果是0 + 0 + 0 = 0並且顯示正確。 然后,當您單擊按鈕時, step更改為1但一次也不會觸發dispatch 盡管如此,結果現在是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>

解決方案是使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