下面的示例是一个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的重用性降低。
谢谢!
答案 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。
setState
。您可以通过其设置器点击状态变量的最新值。setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
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;
有时候应用程序不是那么简单,因此您可能需要使用上述技巧。