> ## Content Index
> Fetch the complete content index at: https://reactpractice.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# When you don't need an effect for setInterval in React
- URL: https://reactpractice.dev/articles/when-you-dont-need-an-effect-for-setinterval-in-react/
- Published: 2024-10-19T21:23:31.000Z
- Updated: 2026-06-01T01:59:14.000Z
- Author: Corina Udrescu
- Tags: Articles, #corina-2, Working with setInterval, #Import 2026-06-09 12:07

Say you want to start a countdown timer when a user presses a "Start button".  
How would you build this in React?

One common way I see - and it's even suggested by ChatGPT - is to have a state variable that remembers if the timer is "on" or "off", and to have an effect watch for that, in order to start the timer.

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/10/chat-gpt-effect.png)

  
Here's the actual code ChatGPT suggests:

```jsx
import React, { useState, useEffect } from 'react';

const CountdownTimer = () => {
  const [timeLeft, setTimeLeft] = useState(60); // Set initial time (in seconds)
  const [isRunning, setIsRunning] = useState(false); // To track if the timer is running
  
  useEffect(() => {
    let timer;
    if (isRunning && timeLeft > 0) {
      timer = setInterval(() => {
        setTimeLeft((prevTime) => prevTime - 1);
      }, 1000); // Decrease time every second
    } else if (timeLeft === 0) {
      clearInterval(timer);
      setIsRunning(false); // Stop timer when time reaches zero
    }

    // Clean up the interval when component unmounts or when time reaches zero
    return () => clearInterval(timer);
  }, [isRunning, timeLeft]);

  const handleStart = () => {
    setIsRunning(true); // Start the countdown
  };

  const handleReset = () => {
    setIsRunning(false); // Stop the timer
    setTimeLeft(60);     // Reset the timer back to 60 seconds
  };

  return (
    <div>
      <h1>Countdown Timer: {timeLeft}s</h1>
      <button onClick={handleStart} disabled={isRunning}>
        Start Countdown
      </button>
      <button onClick={handleReset}>
        Reset
      </button>
    </div>
  );
};

export default CountdownTimer;

```

I think there are several issues with this approach.

### First, effects should be used for synchronisation, whereas starting the counter is a user action

The React docs share [a few examples](https://react.dev/learn/separating-events-from-effects?ref=reactpractice.dev) on how to decide between using an effect or an event handler.

> Intuitively, you could say that event handlers are always triggered “manually”, for example by clicking a button. Effects, on the other hand, are “automatic”: they run and re-run as often as it’s needed to stay synchronized.

In this case, since starting the countdown is trigger by a user action, it can just be handled in the "Start" button click handler. Here's a simple snippet (checkout the end of the article for a full example):

```jsx
const handleStart = () => {
	 // Start the countdown
     setInterval(() => {
        setTimeLeft((prevTime) => prevTime - 1); // Decrease time every second
    }, 1000);
};

```

### Second, using effect to start an interval starts a new interval on every rerender

Let's take a look at the effect in the initial example:

```jsx
useEffect(() => {
    let timer;
    if (isRunning && timeLeft > 0) {
      timer = setInterval(() => {
        setTimeLeft((prevTime) => prevTime - 1);
      }, 1000); // Decrease time every second
    } else if (timeLeft === 0) {
      clearInterval(timer);
      setIsRunning(false); // Stop timer when time reaches zero
    }

    // Clean up the interval when component unmounts or when time reaches zero
    return () => clearInterval(timer);
  }, [isRunning, timeLeft]);

```

This code correctly starts a new interval when `isRunning` goes from `false` to `true`. This is what we would expect.  
But notice that this code also starts a new timer everytime `timeLeft` changes!

The flow would be:

- User clicks "Start" -> `isRunning` changes from `false` to `true`; this triggers a rerender
- Effect is run (due to `isRunning` changed), and since `isRunning` is `true` and `timeLeft > 0`, then a new interval is started (A)
- The interval runs, setting `timeLeft` to `timeLeft` minus 1 second (=59 seconds) ; this triggers a rerender
  - effect cleanup function is called - interval A is cleared
- Effect is run (due to `timeLeft` changed), and since `isRunning` is `true` and `timeLeft > 0`, then a new interval is started (B)
- The interval runs, setting `timeLeft` to `timeLeft` minus 1 second (=58 seconds); this triggers a rerender
  - effect cleanup function is called - interval B is cleared
- Effect is run (due to `timeLeft` changed), and since `isRunning` is `true` and `timeLeft > 0`, then a new interval is started (C)
- The interval runs, setting `timeLeft` to `timeLeft` minus 1 second (=57 seconds); this triggers a rerender
- and so on and so forth

So not only is this code creating and clearing an interval on every render, but as Dan Abramov explains in his article on using intervals in React, this can cause hiccups in the actual interval timing:

> When we run clearInterval and setInterval, their timing shifts. If we re-render and re-apply effects too often, the interval never gets a chance to fire!  
> [https://overreacted.io/making-setinterval-declarative-with-react-hooks/](https://overreacted.io/making-setinterval-declarative-with-react-hooks/?ref=reactpractice.dev)

### So what's a better way to start a countdown? Inside the event handler!

There's no reason why we can't start the interval directly in the event handler.  
This is actually the best place to put side effects:

> Event handlers are the best place for side effects. Unlike rendering functions, event handlers don't need to be pure, so it's a great place to change something  
> [https://react.dev/learn/responding-to-events](https://react.dev/learn/responding-to-events?ref=reactpractice.dev)

Since we will need to also clear the timer, we need to save the `intervalId`. As we don't need the value to trigger a rerender, we can [save it in a ref](https://react.dev/learn/referencing-values-with-refs?ref=reactpractice.dev#when-to-use-refs).

Then, we can clear the interval when the user presses "Reset", in the event handler, or when the component unmounts, in a [cleanup effect](https://react.dev/learn/synchronizing-with-effects?ref=reactpractice.dev#step-3-add-cleanup-if-needed).

```jsx
const intervalRef = useRef(null);

const handleStart = () => {
	 // Start the countdown
    if (intervalRef.current) {
      // if a timer is already running, don't start another one
      return;
    }

    intervalRef.current = setInterval(() => {
      // decrease 1 second from the remaining time
      setTimeLeft((prevTime) => prevTime - 1);
    }, 1000);
};

const handleReset = () => {
    // Stop the timer
	if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }    
    setTimeLeft(60);     // Reset the timer back to 60 seconds
};

// Cleanup the timer on unmount
useEffect(() => {
	return () => {
		clearInterval(intervalRef.current);
	    intervalRef.current = null;
	}
}, [])

```

## Does this mean you should never use intervals with effect?

Not at all! There are still cases where using an effect is the best choice.

For example, In case the interval needs to run outside of a user action - like for a countdown that starts as soon as page is loaded. For this case, your best bet is probably Dan Abramov's `useInterval` hook - [https://overreacted.io/making-setinterval-declarative-with-react-hooks/](https://overreacted.io/making-setinterval-declarative-with-react-hooks/?ref=reactpractice.dev).

How do you work with intervals in React? Share your thoughts in the comments below!

Want to check your understanding of working with intervals in React?   
Check out these challenges:  
 \- [Create a timer that can be started and stopped](https://reactpractice.dev/exercise/create-a-timer-that-can-be-started-and-stopped/)  
 \- [Build a Typewriter effect component](https://reactpractice.dev/exercise/build-a-typewriter-effect-component/)