I wrote a custom React useCountDown hook to count down a number (seconds) until zero. The counting is triggered by start() method attached in a button onClick listener.
It works well without <React.StrictMode>, but not working under strict mode.
import { useCallback, useEffect, useState } from "react";
export default function useCountDown(countdownSeconds = 10) {
const [timing, setTiming] = useState(false);
const [seconds, setSeconds] = useState(countdownSeconds);
const reset = useCallback(() => {
setTiming(true);
setSeconds(countdownSeconds);
}, [countdownSeconds]);
const start = useCallback(() => setTiming(true), []);
useEffect(() => {
let timer: number | undefined;
function countdown() {
setSeconds((preSecond) => {
if (preSecond <= 1) {
setTiming(false);
return countdownSeconds;
} else {
timer = setTimeout(countdown, 1000);
return preSecond - 1;
}
});
}
if (timing) {
timer = setTimeout(countdown, 1000);
}
return () => {
clearTimeout(timer);
};
}, [timing, countdownSeconds]);
return {
start,
seconds,
timing,
reset,
};
}
The count is quickly decreased to zero at no interval (1s) and never stop the counting, under the strict mode. However, it's running well as expected when I removed the <React.StrictMode>.
I found the countdown() executed too many times without interval. Anything wrong in the code? How can I fix the problem?
The codesandbox
I know another approach will be working under the strict mode, like below one. But I'm trying to figure out what went wrong with the above approach.
useEffect(() => {
let timer: NodeJS.Timeout
if (timing) {
timer = setTimeout(() => {
if (seconds > 0) {
setSeconds(seconds - 1)
} else {
onCountdownEndRef.current?.()
setSeconds(countdownSeconds)
setTiming(false)
}
}, 1000)
}
return () => clearTimeout(timer)
}, [seconds, timing, countdownSeconds])
====== Edit =====
Even if I moved the setTiming(false) out of the updater function, it doesn't work either.
useEffect(() => {
seconds === 0 && setTiming(false);
}, [seconds]);
useEffect(() => {
let timer: number | undefined;
function countdown() {
setSeconds((preSecond) => {
if (preSecond <= 1) {
return countdownSeconds;
} else {
timer = setTimeout(countdown, 1000);
return preSecond - 1;
}
});
}
if (timing) {
timer = setTimeout(countdown, 1000);
}
return () => {
clearTimeout(timer);
};
}, [timing, countdownSeconds]);
Isn't it easier to use refs?