如何处理useEffect闭包内的过时状态值?

时间:2020-08-14 13:42:38

标签: reactjs react-hooks

下面的示例是一个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调用中,“ seconds”值将始终等于上次呈现useEffect块时(isActive上次更改时)的值。这将导致setDoubleSeconds(seconds * 2)语句失败。 React Hooks ESLint插件给我有关此问题的警告,内容为:

反应钩子useEffect缺少依赖项:'seconds'。包括它或删除依赖项数组。您也可以更换 如果为'setDoubleSeconds',则将多个useState变量与useReducer一起使用 需要当前的“秒”值。 (react-hooks / exhaustive-deps)eslint

正确的做法是,在依赖项数组中添加“秒”(并将setDoubleSeconds(seconds * 2)更改为setDoubleSeconds((seconds + 1) * )会得到正确的结果。但是,这样做会产生令人讨厌的副作用,导致创建间隔并在每个渲染器上销毁(console.log("Destroying Interval")在每个渲染器上触发)。

因此,现在我正在查看ESLint警告的另一项建议“如果'setDoubleSeconds'需要当前值'seconds',也可以用useReducer替换多个useState变量。

>

我不理解此建议。如果我创建一个reducer并像这样使用它:

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))。

问题

那么对于这种情况有什么建议?我是否会受到性能影响,并仅将“秒”添加到依赖项数组?我是否还要创建另一个依赖于“ seconds”的useEffect块,然后在其中调用“ setDoubleSeconds()”?是否将“秒”和“ doubleSeconds”合并为一个状态对象?我会使用裁判吗?

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

谢谢!

2 个答案:

答案 0 :(得分:0)

  • 我是否对性能产生了影响,只是将“秒”添加到依赖项数组中?
  • 我是否还要创建另一个依赖于“ seconds”的useEffect块并在其中调用“ setDoubleSeconds()”?
  • 我是否可以将“ seconds”和“ doubleSeconds”合并为一个状态对象?
  • 我使用裁判吗?

尽管我个人更愿意选择第二种方法,但所有这些方法都能正常工作

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

但是:

在我的真实应用程序中,doubleSeconds被传递给Child组件,并且我不希望Child组件知道秒如何映射到doubleSeconds,因为它使Child的重用性降低

是有问题的。子组件的实现方式如下:

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

这将是一种更清晰简洁的方法。

答案 1 :(得分:0)

您可以访问效果回调中的值,而无需通过几种方式将其添加为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,而无需触发dep更改。

setDoubleSeconds(secondsRef.current * 2);

我认为您永远不应忽略deps数组的依赖项。如果您不需要更改部门,请使用上述方法来确保您的值是最新的。

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

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

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