在尝试更新名为timer的状态变量时,setInterval() 方法和useEffect钩子没有按预期工作
我在freeCodeCamp上做这个实验 这个实验。我在setInterval() 方法以及useEffect钩子上遇到了问题。一旦我点击按钮,每经过一秒,计时器的状态变量应该减小1,并把这个变化显示在页面上。然而,它只是把它减到1,并在到期值显示前一直固定在该数值。由于这个原因,每当用它的setter函数把5 秒重新加入到计时器状态变量时,它就会被增至超出预期的值。
按钮的onClick事件处理函数所调用的函数里包含setInterval()。每经过一秒,计时器状态变量的setter函数应减小1,直到它达到0。达到0 时,useEffect钩子中的计时器将显示到期信息。
const { useState, useEffect, useRef } = React;
export const OTPGenerator = () => {
const [count, setCounter] = useState(0);
const [timer, setTimer] = useState(5);
const [timerMsg, setTimerMsg] = useState('');
useEffect(() => {
// setTimer(timer - 1);
const countDownTimer = setTimeout(() => {
setTimerMsg("OTP expired. Click the button to generate a new OTP.");
// Once expiration message is displayed, reset the state variable's value back to 5
setTimer(timer + 5);
}, 5000);
return () => clearTimeout(countDownTimer);
}, [count]);
function handleTimer() {
setTimerMsg(`Expires in: ${timer} seconds`);
// Update count since its a dependency and a change will cause the useEffect() hook to run again
setCounter(count + 1);
// Update the timer state variable every second to decrement it from 5 to 0 before displaying expiration message.
setInterval(() => setTimer(timer - 1), 1000);
}
return (
<div className="container">
<h1 id="otp-title">OTP Generator</h1>
<h2 id="otp-display">Click 'Generate OTP' to get a code</h2>
<p id="otp-timer" aria-live=''>{timerMsg}</p>
<button onClick={handleTimer} id="generate-otp-button">Generate OTP</button>
</div>
);
};
目前在控制台中没有显示任何错误。当前的主要问题在于计时器状态变量只在每次按钮点击时减少1,而不是在单次按钮点击后每经过一秒就减少1。我还在代码中添加了注释,以进一步解释我的思路。
解决方案
你把事情想得有点过于复杂了,因为有两个计时器/计数器状态以及相互竞争的超时/间隔定时器。
问题:
正如我在你的帖子中提到的,当时还在Staging Ground,你确实有一些关于陈旧的JavaScript闭包的问题。
然而,它只把它减到1,并在该值上固定不变
这是因为 setInterval(() => setTimer(timer - 1), 1000);,在 handleTimer 回调中闭合的 timer 的值从未改变过,即 timer - 1 总是计算为相同的值。为了解决这个问题,应该使用函数式状态更新,从当前状态正确更新,而不是使用作用域中的闭合值。
每当通过它的setter函数将5 秒重新加入计时器状态变量时,它会被增至高于预期的值
每经过一秒,计时器状态变量的setter函数应递减1,直到它达到0。一旦达到0,useEffect钩子中的计时器将显示到期信息。
这是因为 timer 实际上从未真正递减到0。也没有检查 timer 是否为0 的具体逻辑。当前的超时代码只是有点像在 希望 计时器和状态更新做正确的事情,使得经过5 秒后能够重置。
建议:
- 使用一个单一的计数状态。
- 使用React ref存储一个单一的超时/间隔定时器引用。
- 使用2 个effect,一个在计时器达到0 时管理结束条件,另一个在组件卸载时处理资源清理。注意,这个第二个effect可能并非通过本练习所需,但这是一个良好的习惯。
- 使用函数状态更新,从当前状态值生成更新。
- 不要存储不必要的“状态”。这意味着渲染的
"Expires in: X seconds"可以从当前计数状态推导出来,不需要把信息存储在状态中。通常我们不会在React状态中存储可以从当前状态和/或props推导出的值。直接计算并使用该值,或者如果这是一个“耗时”计算,可以使用useMemo钩子。 - 其余部分遵循练习中规定的用户故事要求。
const { useState, useEffect, useRef } = React;
const OTPGenerator = () => {
const [count, setCount] = useState();
const [otpCode, setOtpCode] = useState();
const timerRef = useRef();
// Check ending "reset" condition.
// This callback is call each time `count` updates in
// order to check if the timer should be expired.
useEffect(() => {
if (count === 0) {
clearInterval(timerRef.current);
setOtpCode(undefined);
}
}, [count]);
// Handle clearing any running timers on component unmount
useEffect(() => {
return () => {
clearInterval(timerRef.current);
}
}, []);
// "Generate" an OTP code (i.e. update the state), set the `count`
// state to a value to count down from, and initiate an interval
// callback to enqueue state updates to actually count down.
const generateOtpCode = () => {
setOtpCode("123456");
setCount(5);
timerRef.current = setInterval(setCount, 1000, c => c - 1)
};
return (
<div className="container">
<h1 id="otp-title">OTP Generator</h1>
<h2 id="otp-display">
{otpCode ?? "Click 'Generate OTP' to get a code"}
</h2>
<p id="otp-timer" aria-live="polite">
{count !== undefined && (
!!count
? `Expires in: ${count} seconds`
: "OTP expired. Click the button to generate a new OTP."
)}
</p>
<button
type="button"
id="generate-otp-button"
disabled={!!count}
onClick={generateOtpCode}
>
Generate OTP
</button>
</div>
)
};
const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<OTPGenerator />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root" />