[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.


