在尝试更新名为timer的状态变量时,setInterval() 方法和useEffect钩子没有按预期工作

前端开发 2026-07-11

我在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 秒后能够重置。

建议:

  1. 使用一个单一的计数状态。
  2. 使用React ref存储一个单一的超时/间隔定时器引用。
  3. 使用2 个effect,一个在计时器达到0 时管理结束条件,另一个在组件卸载时处理资源清理。注意,这个第二个effect可能并非通过本练习所需,但这是一个良好的习惯。
  4. 使用函数状态更新,从当前状态值生成更新。
  5. 不要存储不必要的“状态”。这意味着渲染的 "Expires in: X seconds" 可以从当前计数状态推导出来,不需要把信息存储在状态中。通常我们不会在React状态中存储可以从当前状态和/或props推导出的值。直接计算并使用该值,或者如果这是一个“耗时”计算,可以使用 useMemo 钩子。
  6. 其余部分遵循练习中规定的用户故事要求。
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" />
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章