React components do not leak memory because they render frequently. They leak when something outside React keeps running after the component should be gone. Timers are one of the easiest ways to create that bug because setInterval and setTimeout live in the browser event system, not in React's component lifecycle.
The Classic Timer Leak
This looks harmless:
useEffect(() => {
const id = setInterval(() => {
refreshData();
}, 1000);
}, []);
If the component unmounts, the interval still fires. That means the callback, the captured state, and any work triggered by refreshData() can stay alive longer than intended.
The fix is simple:
useEffect(() => {
const id = setInterval(() => {
refreshData();
}, 1000);
return () => clearInterval(id);
}, []);
Where It Gets Worse
Timer leaks become more expensive when the callback also starts network work:
useEffect(() => {
const id = setInterval(() => {
void refreshData();
}, 5000);
return () => clearInterval(id);
}, [refreshData]);
Now the bug is not just memory. It can become duplicate polling, wasted CPU, and requests still firing after the user has navigated away.
Better Engineering Rule
Any effect that acquires a long-lived resource should release it in the same effect:
- intervals
- timeouts
- event listeners
- observers
- subscriptions
That rule is simple, but it catches a large percentage of React leak bugs. If a resource outlives the render, it should have an explicit cleanup path.
Further Reading