Forgetting to clear timers (like setInterval
) when a component unmounts is a common source of memory leaks in React applications. Here’s how to properly handle timers to avoid these issues:
The Problem
When you don’t clean up timers:
- Memory leaks occur as the timer continues running
- State updates may try to occur on unmounted components
- Performance suffers from unnecessary operations
- Errors appear when trying to update unmounted components
Class Component Solution
class TimerComponent extends React.Component {
intervalId = null;
componentDidMount() {
this.intervalId = setInterval(() => {
// Your interval logic
this.setState({ time: Date.now() });
}, 1000);
}
componentWillUnmount() {
// Clear the interval when component unmounts
clearInterval(this.intervalId);
}
render() {
return <div>Timer Example</div>;
}
}
Functional Component Solution (with Hooks)
function TimerComponent() {
const [time, setTime] = useState(Date.now());
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setTime(Date.now());
}, 1000);
// Cleanup function
return () => {
clearInterval(intervalRef.current);
};
}, []); // Empty dependency array means it runs once on mount
return <div>Current time: {time}</div>;
}
Best Practices
- Always store timer IDs – Save them in instance properties or refs
// Class component
this.timerId = setTimeout(...);
// Functional component
const timerRef = useRef();
timerRef.current = setTimeout(...);
- Clear all timers in cleanup – Both timeouts and intervals
componentWillUnmount() {
clearTimeout(this.timeoutId);
clearInterval(this.intervalId);
}
- Handle dynamic intervals properly:
useEffect(() => {
const id = setInterval(() => {
// Logic using current props
setCount(prev => prev + props.increment);
}, 1000);
return () => clearInterval(id);
}, [props.increment]); // Recreate when increment changes
- Use custom hooks for reusable timer logic:
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Common Pitfalls to Avoid
- Forgetting to clear when intervals are conditionally set:
useEffect(() => {
if (props.active) {
const id = setInterval(...);
// Still need to return cleanup!
return () => clearInterval(id);
}
}, [props.active]);
- Using setTimeout without cleanup:
useEffect(() => {
const id = setTimeout(...);
return () => clearTimeout(id); // Still important!
}, []);
- Recreating timers unnecessarily by missing dependencies:
useEffect(() => {
const id = setInterval(() => {
console.log(props.value); // Uses props.value
}, 1000);
return () => clearInterval(id);
}, []); // Missing props.value dependency
Debugging Timer Leaks
If you suspect timer leaks:
- Check for console warnings about state updates on unmounted components
- Use React DevTools to see if components are unexpectedly staying mounted
- Monitor memory usage in browser dev tools
- Add logging to cleanup functions to verify they’re running