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



下面的例子是一个Timer组件,它有一个按钮(用于启动计时器(和两个标记,分别显示经过的秒数和经过的秒次数2。

然而,它不起作用(CodeSandboxDemo(

代码

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变量需要"秒"的当前值。(反作用挂钩/穷举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(使用Reducers((。

问题

那么,对这种情况的建议是什么呢?我是不是接受了性能上的打击并简单地加上";秒";到依赖数组?我是否创建另一个useEffect块;秒";并呼叫";setDoubleSeconds(("在那里?我是否合并";秒";以及";doubleSeconds";变成一个单一的状态对象?我用裁判吗?

此外,你可能会想";你为什么不简单地改变CCD_;到<h3>Seconds x2: {seconds * 2}</h3>并删除"doubleSeconds"状态&";。在我的实际应用程序中,doubleSeconds被传递给Child组件,我不想让Child组件知道秒是如何映射到doubleSecond的,因为这会降低Child的可重用性。

谢谢!

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

  1. setState。您可以通过状态变量的setter来获取状态变量的最新值
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数组中省略依赖项。如果你需要deps不改变,可以使用上面的方法来确保你的值是最新的。

总是首先考虑是否有比在回调中插入值更优雅的方法来编写代码。在您的示例中,doubleSeconds可以表示为seconds的导数。

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

有时候应用程序并没有那么简单,所以你可能需要使用上面描述的技巧。

  • 我是否接受性能打击并简单地添加"秒";到依赖数组
  • 我是否创建另一个useEffect块;秒";并呼叫";setDoubleSeconds(("在那里
  • 我是否合并";秒";以及";doubleSeconds";变成一个单一的状态对象
  • 我用裁判吗

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

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

但是:

在我的实际应用程序中,doubleSeconds被传递给Child组件,我不想让Child组件知道秒是如何映射到doubleSecond的,因为这会减少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>
);

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

最新更新