This note explains the following code and answers the question about when useEffect runs.
useEffect(() => {
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
if (timeLeft <= 0) {
clearInterval(intervalId);
}
return () => clearInterval(intervalId);
}, [timeLeft]);It is close, but it is not the best pattern for a countdown.
Why:
- Because
[timeLeft]is in the dependency array, the effect runs on the first render and then again every timetimeLeftchanges. - Each time the effect runs, it creates a new interval.
- Before the next effect runs, React cleans up the previous interval.
So this may appear to work, but it is not really using setInterval in the usual way. It behaves more like repeated setTimeout.
There is also a small issue with this line:
setTimeLeft(timeLeft - 1);This uses the timeLeft value captured from that specific render. A safer pattern is to use the functional updater:
setTimeLeft(prev => prev - 1);For countdown logic, setTimeout is usually simpler and cleaner:
useEffect(() => {
if (timeLeft <= 0) return;
const timeoutId = setTimeout(() => {
setTimeLeft(prev => prev - 1);
}, 1000);
return () => clearTimeout(timeoutId);
}, [timeLeft]);- It schedules one tick at a time.
- It avoids creating a long-running interval that gets recreated on every state update.
- It uses
prev => prev - 1, which avoids stale state issues. - It stops naturally when
timeLeftreaches0.
If you want to make sure the value never goes below 0, use this:
useEffect(() => {
if (timeLeft <= 0) return;
const timeoutId = setTimeout(() => {
setTimeLeft(prev => Math.max(prev - 1, 0));
}, 1000);
return () => clearTimeout(timeoutId);
}, [timeLeft]);Yes, useEffect runs after the initial render even when you provide dependencies.
Here is the rule:
useEffect(() => {
console.log("runs after every render");
});This runs:
- after the first render
- after every re-render
useEffect(() => {
console.log("runs once after the first render");
}, []);This runs:
- after the first render only
useEffect(() => {
console.log("runs on mount and when timeLeft changes");
}, [timeLeft]);This runs:
- after the first render
- again whenever
timeLeftchanges
So your understanding is correct:
useEffectdoes run once after the component first renders- dependencies only control when it runs again after that
If your app is running in React Strict Mode during development, useEffect may seem to run twice on mount.
That is expected in development mode and is used by React to help detect unsafe side effects. It does not happen the same way in production.
- Your code is not completely wrong, but it is not the cleanest approach for a countdown.
- For countdowns,
setTimeoutis usually the better choice. useEffectruns after the first render whether you use dependencies or not.- The dependency array controls future reruns, not the first run.