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

bash
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 Route

Common 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

javascript
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