Handling Cleanup in useEffect for Subscriptions
A critical but often overlooked aspect of React’s useEffect
hook is proper cleanup of subscriptions and side effects. Failing to do this can lead to memory leaks, performance issues, and unexpected behavior in your application.
The Problem: Memory Leaks from Unclosed Subscriptions
When you don’t clean up effects:
useEffect(() => {
const subscription = dataStream.subscribe(data => {
setData(data);
});
// ❌ Missing cleanup - subscription remains after component unmounts
}, []);
Consequences:
- Memory leaks (subscriptions keep holding references)
- Attempts to update state in unmounted components
- Performance degradation
- Potential duplicate subscriptions on re-mount
The Solution: Cleanup Function
Every useEffect
can return a cleanup function:
useEffect(() => {
const subscription = dataStream.subscribe(data => {
setData(data);
});
// ✅ Cleanup function
return () => {
subscription.unsubscribe();
};
}, []);
Common Scenarios Requiring Cleanup
1. Event Listeners
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
2. API Subscriptions
useEffect(() => {
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => {
socket.close(); // ✅ Clean up WebSocket
};
}, []);
3. Timers and Intervals
useEffect(() => {
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => {
clearInterval(timer); // ✅
};
}, []);
4. Asynchronous Operations
useEffect(() => {
let isMounted = true; // Flag to track mounted state
fetchData().then(data => {
if (isMounted) { // Only update if component is mounted
setData(data);
}
});
return () => {
isMounted = false; // ✅ Cleanup by setting flag
};
}, []);
Advanced Patterns
1. Combining Multiple Cleanups
useEffect(() => {
const subscription = dataStream.subscribe(handleData);
const timer = setInterval(pollData, 5000);
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, [handleData]);
2. Cleanup with Dependencies
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => response.json())
.then(setData);
return () => {
controller.abort(); // Aborts fetch when url changes or unmounts
};
}, [url]); // ✅ Cleanup runs when url changes
Common Mistakes
❌ Forgetting cleanup for event listeners
useEffect(() => {
document.addEventListener('keydown', handleKeyPress);
// ❌ Missing cleanup
}, [handleKeyPress]);
❌ Assuming cleanup isn’t needed for API calls
useEffect(() => {
fetch(url).then(setData);
// ❌ Should abort if component unmounts
}, [url]);
✅ Correct Approach
useEffect(() => {
let isActive = true;
fetch(url)
.then(response => isActive && setData(response));
return () => {
isActive = false;
};
}, [url]);
Real-World Example
Chat Component with Cleanup
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (msg) => {
setMessages(msgs => [...msgs, msg]);
});
return () => {
connection.disconnect(); // ✅ Clean up when roomId changes or unmounts
};
}, [roomId]); // Effect reruns when roomId changes
return <MessageList messages={messages} />;
}
Why Cleanup Runs on Every Dependency Change
React performs cleanup:
- Before re-running the effect (when dependencies change)
- When the component unmounts
This ensures no stale subscriptions or side effects remain.
Key Takeaways
- Always return a cleanup function from
useEffect
for subscriptions - Clean up:
- Event listeners
- WebSocket/subscription connections
- Timers/intervals
- Async operations
- Use flags (
isMounted
) for async operations - Combine cleanups when multiple resources need disposal
- Remember cleanup runs before re-execution and on unmount
Proper cleanup ensures your React components manage resources efficiently and avoid memory leaks throughout their lifecycle.