# How to Build a Reliable Countdown Timer in React: State Management and Best Practices

> Build a reliable countdown timer in React. Learn to manage state with useEffect and useRef, avoiding stale closures for smooth updates. Master React timer best practices.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: tutorial
- Published: 2026-02-16

---

**Implement a countdown timer in React using `useEffect` to manage the interval lifecycle, `useRef` to store the timer ID without triggering re-renders, and functional state updates to avoid stale closures.**

A countdown timer in React represents a time-based side effect that requires careful management of intervals, state updates, and cleanup to prevent memory leaks. When building a timer in react applications, following the architectural patterns used in the facebook/react source code ensures your component remains declarative, performant, and free of stale closure bugs.

## Core Architecture for a Countdown Timer in React

### useEffect and Cleanup Patterns

The foundation of any reliable countdown timer in React starts with the `useEffect` hook and its cleanup mechanism. As demonstrated in [`fixtures/view-transition/src/components/Page.js`](https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/Page.js), React components must clear intervals when unmounting or when dependencies change to prevent memory leaks.

```jsx
useEffect(() => {
  const timer = setInterval(...);
  return () => clearInterval(timer);
}, []);

```

### useRef for Mutable Timer State

Store the interval identifier in a `useRef` rather than state. This approach mirrors internal React timing utilities found in [`packages/react-reconciler/src/ReactProfilerTimer.js`](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactProfilerTimer.js), where mutable timing data survives re-renders without triggering additional renders.

```jsx
const intervalRef = useRef(null);
intervalRef.current = setInterval(...);

```

### Functional State Updates

Always use the functional updater form `setSeconds(prev => prev - 1)` inside interval callbacks. This pattern prevents stale closures from capturing outdated state values, a technique consistently applied throughout the React codebase including [`fixtures/view-transition/src/components/Page.js`](https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/Page.js).

## Implementation Patterns for a Timer in React

### Basic Countdown Starting on Mount

This implementation creates a simple countdown timer in react that initializes when the component mounts and cleans up automatically on unmount:

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

function Countdown({ initialSeconds }) {
  const [seconds, setSeconds] = useState(initialSeconds);
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setSeconds(prev => {
        if (prev <= 0) {
          clearInterval(intervalRef.current);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  return <div>{seconds}s</div>;
}

```

Key points derived from the React source:
- The interval is created inside `useEffect` to ensure it runs after mount.
- The cleanup function prevents memory leaks when the component unmounts.
- Functional updates ensure the decrement operation always uses the latest state.

### Pausable and Resumable Timer

For a countdown timer in react that supports user controls, watch the `paused` prop in your dependency array:

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

function PausableCountdown({ initialSeconds, paused }) {
  const [seconds, setSeconds] = useState(initialSeconds);
  const intervalRef = useRef(null);

  useEffect(() => {
    if (paused) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
      return;
    }

    intervalRef.current = setInterval(() => {
      setSeconds(prev => {
        if (prev <= 0) {
          clearInterval(intervalRef.current);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, [paused]);

  return <div>{seconds}s</div>;
}

```

The dependency array contains `paused` because the effect must restart the interval when the pause state toggles, matching React's guidance on effect dependencies found in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js).

### Resettable Countdown with Dynamic Targets

When the target time changes, reset the countdown timer in react by including the target in the dependency array:

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

function ResettableCountdown({ targetDate }) {
  const calculateRemaining = () => 
    Math.max(0, Math.floor((targetDate - Date.now()) / 1000));
  
  const [seconds, setSeconds] = useState(calculateRemaining);
  const intervalRef = useRef(null);

  useEffect(() => {
    setSeconds(calculateRemaining());

    intervalRef.current = setInterval(() => {
      setSeconds(prev => {
        if (prev <= 0) {
          clearInterval(intervalRef.current);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, [targetDate]);

  return <div>{seconds}s left</div>;
}

```

This pattern mirrors how React's internal timers listen to configuration changes, as seen in [`packages/react-reconciler/src/ReactProfilerTimer.js`](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactProfilerTimer.js), where timers are restarted when their underlying parameters change.

### High-Precision Timer Using requestAnimationFrame

For sub-second precision in your countdown timer in react, replace `setInterval` with `requestAnimationFrame`:

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

function FineGrainedTimer({ durationMs }) {
  const [remaining, setRemaining] = useState(durationMs);
  const startRef = useRef(performance.now());
  const rafRef = useRef(null);

  const tick = now => {
    const elapsed = now - startRef.current;
    const newRemaining = Math.max(0, durationMs - elapsed);
    setRemaining(newRemaining);
    if (newRemaining > 0) {
      rafRef.current = requestAnimationFrame(tick);
    }
  };

  useEffect(() => {
    startRef.current = performance.now();
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [durationMs]);

  return <div>{(remaining / 1000).toFixed(2)}s</div>;
}

```

This approach is ideal for UI requiring smooth updates, such as progress bars or millisecond-accurate displays. The cleanup discipline remains identical to the `setInterval` patterns found in [`fixtures/view-transition/src/components/Page.js`](https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/Page.js).

## Key React Source Files for Reference

The following files in the facebook/react repository demonstrate the patterns you should follow when building a countdown timer in react:

| File | What you’ll find | Why it matters for timers |
|------|------------------|--------------------------|
| [[`ReactHooks.js`](https://github.com/facebook/react/blob/main/ReactHooks.js)](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) | Source of `useEffect`, `useState`, `useRef` implementations. | Understanding the contract of these hooks helps you use them correctly for timers. |
| [[`Page.js`](https://github.com/facebook/react/blob/main/Page.js)](https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/Page.js) | Real‑world example of `setInterval` inside a `useEffect` with clean‑up. | Directly demonstrates the pattern we recommend. |
| [[`ReactProfilerTimer.js`](https://github.com/facebook/react/blob/main/ReactProfilerTimer.js)](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactProfilerTimer.js) | Internal timer utilities used by the React Profiler. | Shows how React stores mutable timing data and clears it, analogous to using `useRef` for interval IDs. |
| [[`useSyncExternalStoreWithSelector.js`](https://github.com/facebook/react/blob/main/useSyncExternalStoreWithSelector.js)](https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js) | Uses `useEffect` to subscribe to external sources and clean up. | Provides another reference for clean‑up of side‑effects. |

## Summary

- Use `useEffect` with a cleanup function to manage the interval lifecycle and prevent memory leaks when the countdown timer in react unmounts.
- Store the interval ID in `useRef` to maintain a mutable reference that persists across renders without causing re-renders.
- Always use functional state updates (`setState(prev => ...)`) inside interval callbacks to avoid stale closure bugs.
- Include relevant dependencies (like `paused` or `targetDate`) in the `useEffect` dependency array to control when the timer starts, stops, or resets.
- For high-precision countdowns, consider `requestAnimationFrame` instead of `setInterval`, following the same cleanup patterns.

## Frequently Asked Questions

### Why does my countdown timer in react show stale values or skip numbers?

This happens when your interval callback captures a stale closure of the state value. Always use the functional updater form `setSeconds(prev => prev - 1)` to ensure you read the latest state. This pattern mirrors how React's internal schedulers avoid stale reads by referencing mutable refs or latest closures, as implemented in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js).

### Should I use setInterval or setTimeout for a countdown timer in react?

Use `setInterval` for recurring ticks every second, but only if you implement proper cleanup in `useEffect`. While `setTimeout` can work for recursive delays, `setInterval` is more efficient for consistent 1-second updates. Always clear the timer on unmount to prevent memory leaks, following the pattern demonstrated in [`fixtures/view-transition/src/components/Page.js`](https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/Page.js).

### How do I prevent memory leaks when my timer component unmounts?

Return a cleanup function from your `useEffect` that calls `clearInterval(intervalRef.current)` or `cancelAnimationFrame(rafRef.current)`. Store the timer ID in a `useRef` rather than state to avoid re-renders when the ID changes. This follows the same cleanup discipline React uses in [`packages/react-reconciler/src/ReactProfilerTimer.js`](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactProfilerTimer.js) and other internal timing utilities.

### Can I use Date.now() directly inside the interval callback?

Avoid calling `Date.now()` directly inside the interval to calculate remaining time, as this can drift from your component's state and cause synchronization bugs. Instead, calculate the end time once (or use a `targetDate` prop) and decrement a seconds counter, or use a ref to track the start time while updating state only for display purposes. This keeps your UI state as the single source of truth and prevents inconsistencies between the system clock and React's render cycle.