【问题标题】:How to deal with stale state values inside of a useEffect closure?如何处理 useEffect 闭包内的陈旧状态值?
【发布时间】:2020-12-04 09:52:23
【问题描述】:

以下示例是一个 Timer 组件,它有一个按钮(用于启动计时器)和两个显示经过的秒数的标签,以及经过的秒数乘以 2。

但是,它不起作用(CodeSandbox Demo)

守则

import React, { useState, useEffect } from "react";

const Timer = () => {
  const [doubleSeconds, setDoubleSeconds] = useState(0);
  const [seconds, setSeconds] = useState(0);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isActive) {
      interval = setInterval(() => {
        console.log("Creating Interval");
        setSeconds((prev) => prev + 1);
        setDoubleSeconds(seconds * 2);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    return () => {
      console.log("Destroying Interval");
      clearInterval(interval);
    };
  }, [isActive]);

  return (
    <div className="app">
      <button onClick={() => setIsActive((prev) => !prev)} type="button">
        {isActive ? "Pause Timer" : "Play Timer"}
      </button>
      <h3>Seconds: {seconds}</h3>
      <h3>Seconds x2: {doubleSeconds}</h3>
    </div>
  );
};

export { Timer as default };

问题

在 useEffect 调用中,“秒”值将始终等于最后一次渲染 useEffect 块时(最后一次更改 isActive 时)的值。这将导致setDoubleSeconds(seconds * 2) 语句失败。 React Hooks ESLint 插件给了我一个关于这个问题的警告:

React Hook useEffect 缺少依赖项:'seconds'。包括它或删除依赖数组。你也可以换 如果'setDoubleSeconds',使用useReducer 的多个useState 变量 需要“秒”的当前值。 (react-hooks/exhaustive-deps)eslint

正确地,将“秒”添加到依赖数组(并将 setDoubleSeconds(seconds * 2) 更改为 setDoubleSeconds((seconds + 1) * ) 将呈现正确的结果。但是,这有一个令人讨厌的副作用,即导致每个时间间隔都被创建和销毁渲染(console.log("Destroying Interval") 在每次渲染时触发)。

所以现在我正在查看 ESLint 警告中的其他建议“如果 'setDoubleSeconds' 需要 'seconds' 的当前值,您还可以用 useReducer 替换多个 useState 变量”

我不明白这个建议。如果我创建一个减速器并像这样使用它:

import React, { useState, useEffect, useReducer } from "react";

const reducer = (state, action) => {
    switch (action.type) {
        case "SET": {
            return action.seconds;
        }
        default: {
            return state;
        }
    }
};

const Timer = () => {
    const [doubleSeconds, dispatch] = useReducer(reducer, 0);
    const [seconds, setSeconds] = useState(0);
    const [isActive, setIsActive] = useState(false);

    useEffect(() => {
        let interval = null;
        if (isActive) {
            interval = setInterval(() => {
                console.log("Creating Interval");
                setSeconds((prev) => prev + 1);
                dispatch({ type: "SET", seconds });
            }, 1000);
        } else {
            clearInterval(interval);
        }
        return () => {
            console.log("Destroying Interval");
            clearInterval(interval);
        };
    }, [isActive]);

    return (
        <div className="app">
            <button onClick={() => setIsActive((prev) => !prev)} type="button">
                {isActive ? "Pause Timer" : "Play Timer"}
            </button>
            <h3>Seconds: {seconds}</h3>
            <h3>Seconds x2: {doubleSeconds}</h3>
        </div>
    );
};

export { Timer as default };

值过时的问题仍然存在(CodeSandbox Demo (using Reducers))。

问题

那么对于这个场景有什么建议呢?我是否会受到性能影响并简单地将“秒”添加到依赖项数组中?我是否创建另一个依赖于“秒”的 useEffect 块并在其中调用“setDoubleSeconds()”?我是否将“秒”和“双秒”合并到一个状态对象中?我使用 refs 吗?

另外,您可能会想“为什么不简单地将 &lt;h3&gt;Seconds x2: {doubleSeconds}&lt;/h3&gt; 更改为 &lt;h3&gt;Seconds x2: {seconds * 2}&lt;/h3&gt; 并删除 'doubleSeconds' 状态?”。在我的实际应用程序中,doubleSeconds 被传递给子组件,而我没有希望 Child 组件知道如何将秒映射到 doubleSeconds,因为这会降低 Child 的可重用性。

谢谢!

【问题讨论】:

    标签: reactjs react-hooks


    【解决方案1】:

    您可以通过多种方式访问​​效果回调中的值,而无需将其添加为 dep。

    1. setState。您可以通过其设置器来挖掘状态变量的最新值。
    setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
    
    1. 参考。您可以将 ref 作为依赖项传递,它永远不会改变。不过,您需要手动使其保持最新状态。
    const secondsRef = useRef(0);
    const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
    

    然后您可以在代码块中使用secondsRef.current 访问seconds,而无需触发deps 更改。

    setDoubleSeconds(secondsRef.current * 2);
    

    在我看来,你永远不应该忽略 deps 数组中的依赖项。如果您需要不更改部门,请使用上述技巧来确保您的值是最新的。

    始终首先考虑是否有比将值写入回调更优雅的方式来编写代码。在您的示例中,doubleSeconds 可以表示为seconds 的派生词。

    const [seconds, setSeconds] = useState(0);
    const doubleSeconds = seconds * 2;
    

    有时应用程序并不那么简单,因此您可能需要使用上述技巧。

    【讨论】:

      【解决方案2】:
      • 我是否会受到性能影响并简单地将“秒”添加到依赖项数组中?
      • 我是否创建另一个依赖于“秒”的 useEffect 块并在其中调用“setDoubleSeconds()”?
      • 我是否将“seconds”和“doubleSeconds”合并为一个状态对象?
      • 我使用 refs 吗?

      它们都可以正常工作,尽管我个人更愿意选择第二种方法:

      useEffect(() => {
          setDoubleSeconds(seconds * 2);
      }, [seconds]);
      

      但是:

      在我的实际应用程序中,doubleSeconds 被传递给子组件,我不希望子组件知道秒数是如何映射到 doubleSeconds 的,因为它会降低子组件的可重用性

      有问题的。子组件可能实现如下:

      const Child = ({second}) => (
        <p>Seconds: {second}s</p>
      );
      

      父组件应该如下所示:

      const [seconds, setSeconds] = useState(0);
      useEffect(() => {
        // change seconds
      }, []);
      
      return (
        <React.Fragment>
          <Child seconds={second} />
          <Child seconds={second * 2} />
        </React.Fragment>
      );
      

      这样会更简洁明了。

      【讨论】:

        猜你喜欢
        • 2020-12-16
        • 2020-07-02
        • 1970-01-01
        • 2019-11-08
        • 1970-01-01
        • 2020-12-30
        • 1970-01-01
        • 1970-01-01
        • 2021-06-24
        相关资源
        最近更新 更多