almessadi.
Back to Index

React Timer Leaks Happen When Effects Start Work They Never Stop_

Intervals, observers, and long-lived browser callbacks can keep memory and CPU work alive after unmount unless the effect cleans them up explicitly.

PublishedNovember 5, 2024
Reading Time9 min read

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