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
- Memory leaks – The timer keeps running after component unmounts
- State updates on unmounted components – Causes React warnings
- Performance issues – Wasted resources from orphaned timers
- 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
- Forgetting the cleanup:
useEffect(() => {
setInterval(/*...*/);
// Missing return cleanup function
}, []);
- Using stale state:
useEffect(() => {
setInterval(() => {
setCount(count + 1); // ❌ Uses stale count
}, 1000);
// ...
}, []);
- 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>;
}