DEV Community

Cover image for Fix: Can't Perform a State Update on an Unmounted Component
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: Can't Perform a State Update on an Unmounted Component

TL;DR

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 means a .then(), setTimeout, or subscription callback ran after the component that started it was removed from the tree, and tried to call setState anyway. The fix is to cancel the operation in your useEffect cleanup function — not to gate the setState call with an if check, which hides the leak instead of closing it.

  • Symptom: Console warning naming the exact component, usually right after navigating away from a page mid-fetch
  • Root cause: An async operation (fetch, timer, subscription) outlives the component and calls setState on unmount
  • Fix: Cancel the operation in the useEffect cleanup — AbortController for fetch, clearTimeout for timers, unsubscribe() for subscriptions
  • Anti-pattern: An isMounted boolean ref silences the warning but leaves the underlying request running to completion, wasting the network call

Why this is one of the highest-traffic React questions ever asked

The Stack Overflow question "Can't perform a React state update on an unmounted component" has over 774,000 views and 436 upvotes — it is one of the most-viewed React troubleshooting questions on the entire site. That volume exists because almost every data-fetching component has this bug latent inside it from the first draft: fetch on mount, setState when the response arrives, done. The bug only surfaces once a user navigates away before the response lands — fast networks and local development hide it for weeks.

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 the
componentWillUnmount method.
Enter fullscreen mode Exit fullscreen mode

The message names componentWillUnmount because it predates hooks, but the equivalent for function components is the cleanup function returned from useEffect.

The bug, isolated

// ❌ classic version of the bug
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data)); // fires even if unmounted by now
  }, [userId]);

  return user ? <div>{user.name}</div> : <div>Loading…</div>;
}
Enter fullscreen mode Exit fullscreen mode

If the user clicks away from this component before the fetch resolves, the .then() callback still runs — closures don't know the component is gone — and setState fires against a component React has already torn down.

The anti-pattern: the isMounted ref

This is the fix that shows up in most of the older Stack Overflow answers, and it silences the warning without actually solving the leak:

// ⚠️ suppresses the warning but the fetch still completes uselessly
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const isMounted = useRef(true);

  useEffect(() => {
    isMounted.current = true;
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (isMounted.current) setUser(data);
      });

    return () => {
      isMounted.current = false;
    };
  }, [userId]);
}
Enter fullscreen mode Exit fullscreen mode

The setState call is guarded, so React never complains — but the fetch request itself keeps running on the network and the server keeps doing the work of answering it. On a page with fast, repeated navigation (a search-as-you-type list, a tab switcher), this pattern accumulates in-flight requests that do nothing useful. It is a real fix for the symptom, not the cause.

The fix: cancel instead of check

AbortController cancels the request itself, which is what an unmounted component should actually do with work it no longer needs:

// ✅ cancels the request when the component unmounts
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => res.json())
      .then((data) => setUser(data))
      .catch((err) => {
        if (err.name !== 'AbortError') throw err; // ignore the cancellation itself
      });

    return () => controller.abort();
  }, [userId]);

  return user ? <div>{user.name}</div> : <div>Loading…</div>;
}
Enter fullscreen mode Exit fullscreen mode

When the component unmounts (or userId changes, re-running the effect), controller.abort() cancels the in-flight request. The browser stops downloading the response, the .then() chain never reaches setUser, and no warning fires — because the state update genuinely never happens, rather than being silently swallowed.

The same pattern for timers and subscriptions

setTimeout/setInterval and any subscription-based API (WebSockets, Supabase Realtime channels, EventSource) hit the identical bug and need the identical shape of fix — clear or unsubscribe in the cleanup, never guard the callback:

// Timer
useEffect(() => {
  const id = setTimeout(() => setStatus('done'), 3000);
  return () => clearTimeout(id);
}, []);

// Subscription (e.g. Supabase Realtime)
useEffect(() => {
  const channel = supabase
    .channel('room-1')
    .on('broadcast', { event: 'message' }, (payload) => setMessages((m) => [...m, payload]))
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Libraries that already solve this for you

If most of an app's data fetching goes through raw useEffect + fetch, migrating to a data-fetching library that owns the cancellation lifecycle removes this entire class of bug rather than requiring an AbortController at every call site. TanStack Query and SWR both cancel or ignore stale requests internally when a component unmounts or a query key changes — this is one of the concrete reasons those libraries exist rather than being a thin wrapper over fetch.

Verifying the fix

  1. Throttle the network to "Slow 3G" in DevTools, trigger the fetch, then navigate away before it resolves. No warning should print, and the Network tab should show the request as cancelled, not completed.
  2. grep -rn "useEffect" src for any effect that calls fetch without an AbortController — each one is a candidate for the same bug even if it hasn't surfaced yet.
  3. For subscription-based effects, confirm the cleanup function actually unsubscribes — a cleanup that returns nothing, or that recreates the subscription without removing the old one, reintroduces the leak silently.

Related Articles


Originally published at https://www.iloveblogs.blog

Top comments (0)