Skip to content

Instantly share code, notes, and snippets.

@surajbera
Created June 7, 2026 03:00
Show Gist options
  • Select an option

  • Save surajbera/a93dad8ddc885976f12cb1a2c808ceda to your computer and use it in GitHub Desktop.

Select an option

Save surajbera/a93dad8ddc885976f12cb1a2c808ceda to your computer and use it in GitHub Desktop.
useEffect timer readme

useEffect Countdown Notes

This note explains the following code and answers the question about when useEffect runs.

The Original Code

useEffect(() => {
  const intervalId = setInterval(() => {
    setTimeLeft(timeLeft - 1);
  }, 1000);

  if (timeLeft <= 0) {
    clearInterval(intervalId);
  }

  return () => clearInterval(intervalId);
}, [timeLeft]);

Is This Code Right?

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 time timeLeft changes.
  • 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);

Better Version for a Countdown

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]);

Why This Version Is Better

  • 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 timeLeft reaches 0.

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]);

When Does useEffect Run?

Yes, useEffect runs after the initial render even when you provide dependencies.

Here is the rule:

1. No dependency array

useEffect(() => {
  console.log("runs after every render");
});

This runs:

  • after the first render
  • after every re-render

2. Empty dependency array

useEffect(() => {
  console.log("runs once after the first render");
}, []);

This runs:

  • after the first render only

3. Dependency array with values

useEffect(() => {
  console.log("runs on mount and when timeLeft changes");
}, [timeLeft]);

This runs:

  • after the first render
  • again whenever timeLeft changes

So your understanding is correct:

  • useEffect does run once after the component first renders
  • dependencies only control when it runs again after that

Important Extra Note About Strict Mode

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.

Final Takeaway

  • Your code is not completely wrong, but it is not the cleanest approach for a countdown.
  • For countdowns, setTimeout is usually the better choice.
  • useEffect runs after the first render whether you use dependencies or not.
  • The dependency array controls future reruns, not the first run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment