The Problem
When an async operation (API call, timer, subscription) completes after a component has unmounted, calling setState on that unmounted component triggers a warning and causes memory leaks.
Symptoms
- Console warning: "Can't perform a React state update on an unmounted component"
- Memory usage grows over time as you navigate between pages
- React DevTools shows components persisting in memory after unmount
- Data from a previous page appears on the current page briefly
Real Error Message
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a
useEffect cleanup function.
at UserProfile
at RouteCommon Scenario
```javascript // BAD: Fetch continues after component unmounts function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { fetchUser(userId).then(data => { setUser(data); // Runs even if component unmounted! setLoading(false); // Runs even if component unmounted! }); }, [userId]);
if (loading) return <Spinner />; return <div>{user.name}</div>; } ```
If the user navigates away before the fetch completes, the then callback still runs and calls setUser and setLoading on the unmounted component.
How to Fix It
Fix 1: AbortController for Fetch Requests (Recommended)
```javascript function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { const controller = new AbortController();
fetch(/api/users/${userId}, { signal: controller.signal })
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
setLoading(false);
}
});
return () => controller.abort(); // Cleanup: aborts pending request }, [userId]);
if (loading) return <Spinner />; return <div>{user?.name}</div>; } ```
Fix 2: Cleanup Flag for Non-Fetch Async
```javascript useEffect(() => { let cancelled = false;
async function load() { const data = await someAsyncOperation(); if (!cancelled) { setData(data); } }
load(); return () => { cancelled = true; }; }, []); ```
Fix 3: Cleanup Event Listeners and Timers
useEffect(() => {
const interval = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(interval); // Cleanup prevents setState on unmount
}, []);Prevention
- Always return a cleanup function from useEffect for async work
- Use AbortController for all fetch/XHR requests
- Clear intervals, timeouts, and subscriptions in cleanup
- Consider using React Query or SWR which handle this automatically