Back to list
ReactuseEffectuseRefsetIntervalPolling

[React][Util] Control setInterval with useEffect and useRef

Build safe polling with async/await, store timer IDs in useRef, and always clearInterval on cleanup so zombie timers do not leak memory or hammer your API.

1. Environment

  • OS: Windows 11 / macOS
  • Frontend library: React.js (v18+)
  • Core concepts: Async I/O (async/await), timers (setInterval, clearInterval), polling
  • IDE: Visual Studio Code (VS Code)

2. Problem

Product requirements often say: “Refresh the UI with the latest server data on a schedule.|–

Examples: live stock charts, chat windows, unread-notification badges.

If you spin timers carelessly, you can fire the API hundreds of times (server load) or fill browser memory until the app freezes.

This post shows how to combine async/await, setInterval, and clearInterval for safe periodic work.

3. Cause

Crashes and runaway requests usually come from zombie timers“timers you started but never stopped.

A React component mounts, updates, then unmounts when you leave the page. If the timer keeps running after unmount, it keeps calling APIs in the background and multiplies.

You need a clear split of roles:

Tool Role
async / await Request data and wait for the response
setInterval(fn, ms) Repeat fn every ms; returns a timer ID
clearInterval(id) Stop that specific timer (the brake)

4. Solution

Combine useEffect lifecycle management with useRef (store a value without re-rendering) for a safe polling pattern.

The example below refreshes a Bitcoin price every 5 seconds, can be paused with a button, and clears the timer automatically when the screen goes away.

Why useRef for the timer ID?
You need the ID later for clearInterval. Putting it in useState would re-render on every save; a plain let would reset on re-render and you would lose the timer forever.

Why return () => clearInterval(...)?
Without that cleanup line you create zombie timers. It is the main memory-leak shield in React timer work.

import React, { useState, useEffect, useRef } from 'react';

function RealTimeDashboard() {
  const [price, setPrice] = useState(0);
  const [isPolling, setIsPolling] = useState(true);

  // 1. Notebook for the timer ID - no re-render on write
  const timerRef = useRef(null);

  // 2. Async fetch
  const fetchPrice = async () => {
    try {
      console.log('Fetching latest price...');
      // Example real call:
      // const response = await fetch('https://api.example.com/price');
      // const data = await response.json();

      // Demo: random price
      const randomPrice = Math.floor(Math.random() * 10000) + 80000000;
      setPrice(randomPrice);
    } catch (error) {
      console.error('Fetch error:', error);
    }
  };

  useEffect(() => {
    if (isPolling) {
      // Run once immediately when polling starts
      fetchPrice();

      // 3. Repeat every 5s; keep ID off the render path
      timerRef.current = setInterval(() => {
        fetchPrice();
      }, 5000);
    }

    // 4. Cleanup: brake when unmounting or when isPolling changes
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
        console.log('Timer cleared safely.');
      }
    };
  }, [isPolling]);

  const togglePolling = () => {
    setIsPolling(!isPolling);
  };

  return (
    <div>
      <h2>Bitcoin price: {price.toLocaleString()} KRW</h2>
      <button onClick={togglePolling}>
        {isPolling ? 'Stop live updates' : 'Start live updates'}
      </button>
    </div>
  );
}

export default RealTimeDashboard;

Related: useRef without re-renders · async/await for API calls

5. Summary

The absolute rule for timers in JavaScript/React: if you set it, you must clear it.

For stock charts, monitoring dashboards, or periodic animations, pair async/await with setInterval / clearInterval this way and you can leave the page open without memory leaks from runaway timers.

Related posts