The Critical Importance of Cleaning Up Timers in React
Forgetting to clear timers (setInterval
, setTimeout
) when a component unmounts is one of the most common sources of memory leaks and bugs in React applications. Here’s a comprehensive guide to proper timer management:
Why This Matters
The Problem:
function TimerComponent() {
const [count, setCount] = useState(0);
// DANGER: Timer continues after unmount!
setInterval(() => {
setCount(c => c + 1); // Will try to update unmounted component
}, 1000);
return <div>{count}</div>;
}
Consequences:
- Memory leaks (Timer callbacks keep running)
- State updates on unmounted components (React warnings/errors)
- Performance degradation (Multiple orphaned timers accumulate)
- Stale closures (Outdated values in timer callbacks)
The Solution: Proper Cleanup
1. Class Components
class TimerComponent extends React.Component {
intervalId = null;
componentDidMount() {
this.intervalId = setInterval(() => {
this.setState({ count: this.state.count + 1 });
}, 1000);
}
// ✅ MUST clean up
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <div>{this.state.count}</div>;
}
}
2. Function Components (Hooks)
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ Using functional update
}, 1000);
// ✅ Cleanup function
return () => clearInterval(intervalId);
}, []); // Empty deps = runs once on mount
return <div>{count}</div>;
}
Advanced Patterns
1. Dynamic Interval Timing
function DynamicTimer({ delay }) {
useEffect(() => {
const intervalId = setInterval(/* ... */, delay);
return () => clearInterval(intervalId);
}, [delay]); // ✅ Re-creates timer when delay changes
}
2. Pausable Timer
function PausableTimer() {
const [count, setCount] = useState(0);
const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
if (isPaused) return; // Skip if paused
const intervalId = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalId);
}, [isPaused]); // ✅ Reconfigures when pause state changes
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setIsPaused(p => !p)}>
{isPaused ? 'Resume' : 'Pause'}
</button>
</div>
);
}
3. setTimeout Cleanup
function TimeoutComponent() {
useEffect(() => {
const timeoutId = setTimeout(() => {
// Do something
}, 5000);
return () => clearTimeout(timeoutId);
}, []);
}
Common Pitfalls & Solutions
1. Async Operations in Timers
// Problem: Async operation might complete after unmount
useEffect(() => {
const intervalId = setInterval(async () => {
const data = await fetchData();
setData(data); // Risk of state update on unmounted component
}, 5000);
return () => clearInterval(intervalId);
}, []);
// ✅ Solution: Add mounted check
useEffect(() => {
let isMounted = true;
const intervalId = setInterval(async () => {
const data = await fetchData();
if (isMounted) setData(data);
}, 5000);
return () => {
isMounted = false;
clearInterval(intervalId);
};
}, []);
2. Multiple Timers
// ✅ Proper management of multiple timers
useEffect(() => {
const timer1 = setInterval(/* ... */);
const timer2 = setInterval(/* ... */);
return () => {
clearInterval(timer1);
clearInterval(timer2);
};
}, []);
Debugging Timer Leaks
1. Detection Methods
- React DevTools: Check for components that stay mounted
- Console warnings: “Can’t perform state update on unmounted component”
- Performance monitoring: Memory usage growing over time
2. ESLint Rule
Enable this rule to catch missing cleanups:
{
"react-hooks/exhaustive-deps": "error"
}
Best Practices
- Always write the cleanup first before adding the effect
- Use functional updates (
setCount(c => c + 1)
) to avoid stale values - Consider custom hooks for reusable timer logic:
function useInterval(callback, delay) {
useEffect(() => {
const intervalId = setInterval(callback, delay);
return () => clearInterval(intervalId);
}, [callback, delay]);
}
// Usage:
useInterval(() => {
setCount(c => c + 1);
}, 1000);
- For complex timing, consider libraries like:
Key Takeaways
- Every
setInterval
needs a matchingclearInterval
- Cleanup belongs in
componentWillUnmount
oruseEffect
‘s return - Functional updates prevent stale state
- Mounted checks protect against async updates after unmount
- Custom hooks simplify timer management