# How to Build a Reliable React Timer with Hooks for Countdown Functionality

> Learn to build a reliable React timer with hooks for countdowns. Use useEffect and useRef to manage intervals and ensure accurate functionality.

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

---

**Use `useEffect` with a cleanup function that calls `clearInterval` to manage intervals, store the timer ID in a `useRef` to prevent duplicate instances, and initialize timers only after component mount using an empty dependency array.**

Managing timers in React function components requires careful coordination of side effects to prevent memory leaks, stale closures, and duplicate intervals. The `facebook/react` repository demonstrates the canonical implementation pattern in [`fixtures/nesting/src/shared/useTime.js`](https://github.com/facebook/react/blob/main/fixtures/nesting/src/shared/useTime.js), showing how to leverage `useEffect` for lifecycle management and `useRef` for stable timer references. By following these patterns, you can build countdown functionality that starts and stops reliably without leaking memory or causing unexpected re-renders.

## The Canonical Timer Pattern in React

The React team maintains a reference implementation of a timer hook in the repository's fixture code. In [`fixtures/nesting/src/shared/useTime.js`](https://github.com/facebook/react/blob/main/fixtures/nesting/src/shared/useTime.js), the implementation demonstrates the essential pattern for reliable timer management: initializing the interval inside `useEffect`, storing the interval ID, and returning a cleanup function.

This pattern ensures that the timer starts exactly once when the component mounts and cleans up automatically when the component unmounts, preventing the common pitfall of intervals continuing to run after the component is gone.

## Building a Basic Timer Hook

The [`useTime.js`](https://github.com/facebook/react/blob/main/useTime.js) fixture provides a minimal, reusable hook that updates every second. This implementation serves as the foundation for any timer-based functionality, including countdowns.

```javascript
import {useState, useEffect} from 'react';

export default function useTimer() {
  const [value, setValue] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => {
      setValue(new Date());
    }, 1000);
    return () => clearInterval(id);   // ← cleanup on unmount
  }, []);                              // ← empty deps → run once

  return value.toLocaleTimeString();
}

```

**Key implementation details from the React source:**

- **Start only once**: The empty dependency array (`[]`) ensures the effect runs only after the initial render, preventing duplicate intervals.
- **Clean up**: The returned function calls `clearInterval(id)`, guaranteeing the timer stops when the component unmounts or when the effect re-runs.
- **State updates**: `setValue` triggers a re-render with the new time value on each interval tick.
- **Reference stability**: The interval ID is stored in a local variable (`id`) scoped to the effect, avoiding recreation on every render.

## Implementing a Countdown Timer with Start/Stop Controls

To create a countdown that can be started, paused, and reset, extend the basic pattern by adding state for the remaining time and control flags. This implementation uses `useRef` to store the interval ID persistently across renders without triggering re-renders.

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

/**
 * useCountdown
 * @param {number} startSeconds – initial countdown value in seconds
 * @param {boolean} autoStart – start the timer immediately when mounted
 * @returns {{
 *   time: number,
 *   start: () => void,
 *   pause: () => void,
 *   reset: (seconds?: number) => void,
 *   isRunning: boolean
 * }}
 */
export function useCountdown(startSeconds: number, autoStart = true) {
  const [time, setTime] = useState(startSeconds);
  const [isRunning, setRunning] = useState(autoStart);
  const intervalRef = useRef<number | null>(null);

  const clear = useCallback(() => {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }, []);

  // Start / pause logic
  const start = useCallback(() => {
    if (intervalRef.current !== null) return; // already running
    setRunning(true);
    intervalRef.current = window.setInterval(() => {
      setTime(prev => {
        if (prev <= 1) {
          clear();               // stop at 0
          setRunning(false);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);
  }, [clear]);

  const pause = useCallback(() => {
    clear();
    setRunning(false);
  }, [clear]);

  const reset = useCallback((seconds?: number) => {
    clear();
    setTime(seconds ?? startSeconds);
    if (autoStart) start();
  }, [clear, start, startSeconds, autoStart]);

  // Cleanup on unmount (mirrors the pattern in useTimer)
  useEffect(() => {
    if (autoStart) start();
    return clear; // <-- guarantees no stray intervals
  }, [autoStart, start, clear]);

  return { time, start, pause, reset, isRunning };
}

```

**Usage in a component:**

```tsx
import React from 'react';
import { useCountdown } from './useCountdown';

export default function CountdownDisplay() {
  const { time, start, pause, reset, isRunning } = useCountdown(30, false);

  return (
    <div>
      <h1>{time}s</h1>
      <button onClick={isRunning ? pause : start}>
        {isRunning ? 'Pause' : 'Start'}
      </button>
      <button onClick={() => reset(30)}>Reset to 30s</button>
    </div>
  );
}

```

## Key Source Files in the React Repository

The following files from the `facebook/react` repository demonstrate the patterns and constraints governing hook-based timers:

- **[`fixtures/nesting/src/shared/useTime.js`](https://github.com/facebook/react/blob/main/fixtures/nesting/src/shared/useTime.js)** – Contains the canonical `useTimer` implementation showing interval initialization and cleanup.
- **[`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js)** – Internal hook registration logic that explains why stable references matter for effects.
- **[`packages/react-devtools-timeline/src/Timeline.js`](https://github.com/facebook/react/blob/main/packages/react-devtools-timeline/src/Timeline.js)** – Practical example of interval usage in the DevTools timeline feature.
- **[`packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js`](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js)** – Test suite enforcing the Rules of Hooks, ensuring your timer logic respects dependency arrays.

## Summary

- **Use `useEffect`** to encapsulate timer side effects, ensuring they start after render and clean up on unmount.
- **Return a cleanup function** from `useEffect` that calls `clearInterval` to prevent memory leaks and duplicate timers.
- **Store interval IDs** in `useRef` or local effect variables to maintain stable references across renders.
- **Guard against duplicates** by checking if an interval is already running before starting a new one.
- **Follow the pattern** demonstrated in [`fixtures/nesting/src/shared/useTime.js`](https://github.com/facebook/react/blob/main/fixtures/nesting/src/shared/useTime.js) from the `facebook/react` repository for canonical implementation details.

## Frequently Asked Questions

### Why does my React timer run multiple times or speed up unexpectedly?

This happens when you start a new interval on every render without cleaning up the previous one. Always store the interval ID in a `useRef` or return a cleanup function from `useEffect` that calls `clearInterval`. The [`useTime.js`](https://github.com/facebook/react/blob/main/useTime.js) fixture in the React repository demonstrates the correct pattern of using an empty dependency array to ensure the effect runs only once.

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

Both work, but `setInterval` is more convenient for recurring ticks like a countdown. However, `setTimeout` can be safer if you need dynamic interval changes because it doesn't risk overlapping calls if the component re-renders slowly. Regardless of which you choose, always clear the timer in the `useEffect` cleanup function to match the pattern shown in [`fixtures/nesting/src/shared/useTime.js`](https://github.com/facebook/react/blob/main/fixtures/nesting/src/shared/useTime.js).

### How do I pause and resume a timer without resetting the value?

Store the interval ID in a `useRef` so it persists across renders without triggering re-renders. Create `start` and `pause` callbacks that check `intervalRef.current` to prevent duplicate intervals. When pausing, call `clearInterval(intervalRef.current)` and set the ref to `null`. When starting, only create a new interval if the ref is `null`. This approach mirrors the control logic in the `useCountdown` implementation derived from React's core patterns.

### Why is cleanup important when using timers in React?

React components mount and unmount dynamically, especially in single-page applications with routing. If you don't clear intervals when a component unmounts, the JavaScript engine continues executing the callback, trying to update state on a component that no longer exists. This causes memory leaks and "Can't perform a React state update on an unmounted component" warnings. The `useEffect` cleanup function, as demonstrated in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) and the [`useTime.js`](https://github.com/facebook/react/blob/main/useTime.js) fixture, ensures resources are released when the component lifecycle ends.