Search code examples
reactjsreact-hooksuse-effect

a count-down component with useEffect not working as expected


I'm learning to react hooks. I write a count-down component. useEffect makes me confused. when the timer turns to 00:00, it can't show 00:00, just turn to 00:10. anyone can help? below is a demo.

count-down enter image description here

export default function App() {
  const [timer, setTimer] = useState(dayjs().minute(0).second(10));
  const [status, setStatus] = useState(true);

  const handleReset = () => {
    setStatus(true);
    setTimer(dayjs().minute(0).second(10));
  };
  const handleStartStop = () => {
    setStatus((pre) => !pre);
  };

  useEffect(() => {
    setTimer((pre) => {
      if (pre.format("mm:ss") === "00:00") {
        return dayjs().minute(0).second(10);
      }
      return pre;
    });
  }, [timer]);

  useEffect(() => {
    if (!status) {
      const intervalId = setInterval(() => {
        setTimer((pre) => {
          return pre.subtract(1, "second");
        });
      }, 1000);
      return () => {
        clearInterval(intervalId);
      };
    }
  }, [status]);

  return (
    <>
      <div id="timer-label">
        count down
        <div id="time-left">{timer.format("mm:ss")}</div>
        <div>
          <button id="start_stop" onClick={() => handleStartStop()}>
            {status ? "start" : "stop"}
          </button>
          <button id="reset" onClick={handleReset}>
            reset
          </button>
        </div>
      </div>
    </>
  );
}


Solution

  • Issue

    When you split and use 2 useEffect hooks, it seems there is some clock skew between the effects and the timing of checking for "00:00" and resetting is out of sync with the interval.

    useEffect(() => console.log('----- render -----'));
    
    useEffect(() => {
      setTimer((pre) => {
        console.log('effect 2', pre.format("mm:ss"))
        if (pre.format("mm:ss") === "00:00") {
          return dayjs().minute(0).second(10);
        }
        return pre;
      });
    }, [timer]);
    
    useEffect(() => {
      if (!status) {
        const intervalId = setInterval(() => {
          setTimer((pre) => {
            console.log('effect 1', pre.format("mm:ss"))
            return pre.subtract(1, "second");
          });
        }, 1000);
        return () => {
          clearInterval(intervalId);
        };
      }
    }, [status]);
    

    These logs yield:

    ----- render ----- 
    effect 1 00:10 
    ----- render ----- 
    effect 2 00:09 
    effect 1 00:09 
    ----- render ----- 
    effect 2 00:08 
    effect 1 00:08 
    ----- render ----- 
    effect 2 00:07 
    ----- render ----- 
    effect 2 00:06 
    effect 1 00:07 
    ----- render ----- 
    effect 2 00:05 
    effect 1 00:06 
    ----- render ----- 
    effect 2 00:04 
    effect 1 00:05 
    ----- render ----- 
    effect 2 00:03 
    effect 1 00:04 
    ----- render ----- 
    effect 2 00:02 
    effect 1 00:03 
    ----- render ----- 
    effect 2 00:01 
    effect 1 00:02 
    ----- render ----- 
    effect 2 00:00  // <-- this one is also not in sync
    ----- render ----- 
    effect 2 00:10  // <-- immediately reset 
    effect 1 00:01 
    

    This issue and skew is resolved by placing all the logic in a single useEffect.

    useEffect(() => {
      if (!status) {
        const intervalId = setInterval(() => {
          setTimer((pre) => {
            if (pre.format("mm:ss") === "00:00") {
              return dayjs().minute(0).second(10);
            }
            return pre.subtract(1, "second");
          });
        }, 1000);
        return () => {
          clearInterval(intervalId);
        };
      }
    }, [status]);
    

    Edit a-count-down-component-with-useeffect-not-working-as-expected