下面的例子是一个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。
setState
。您可以通过状态变量的setter来获取状态变量的最新值
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
- 参考。您可以将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>
);
这将是一种更加清晰简洁的方式。