Forgetting to remove timers (setInterval) on unmount

Loading

Forgetting to Remove Timers on Unmount in React

The Memory Leak Problem

A common React mistake is creating timers with setInterval but forgetting to clean them up when components unmount:

function TimerComponent() {
  const [count, setCount] = useState(0);

  // ❌ Dangerous - timer never gets cleaned up
  setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return <div>{count}</div>;
}

Why This Is Dangerous

  1. Memory leaks – The timer keeps running after component unmounts
  2. State updates on unmounted components – Causes React warnings
  3. Performance issues – Wasted resources from orphaned timers
  4. Stale closures – May capture outdated state/props

The Proper Solution

Class Components:

class TimerComponent extends React.Component {
  intervalId = null;

  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState({ count: this.state.count + 1 });
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.intervalId); // ✅ Cleanup
  }
}

Functional Components:

function TimerComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(intervalId); // ✅ Cleanup
  }, []);

  return <div>{count}</div>;
}

Common Pitfalls to Avoid

  1. Forgetting the cleanup:
   useEffect(() => {
     setInterval(/*...*/);
     // Missing return cleanup function
   }, []);
  1. Using stale state:
   useEffect(() => {
     setInterval(() => {
       setCount(count + 1); // ❌ Uses stale count
     }, 1000);
     // ...
   }, []);
  1. Recreating timers unnecessarily:
   useEffect(() => {
     const id = setInterval(/*...*/);
     return () => clearInterval(id);
   }, [someProp]); // ❌ Timer recreated when someProp changes

Advanced Patterns

Custom Hook Solution:

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

// Usage:
function TimerComponent() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return <div>{count}</div>;
}

Dynamic Interval Control:

function TimerComponent({ active }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (!active) return;

    const intervalId = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [active]); // ✅ Recreates timer only when active changes

  return <div>{count}</div>;
}

Leave a Reply

Your email address will not be published. Required fields are marked *