Can’t Perform A React State Update On An Unmounted Component Warning – Unmounted Component State Update

React warns about state updates on unmounted components when asynchronous operations complete after the component leaves the DOM. This “Can’t Perform A React State Update On An Unmounted Component Warning” is one of the most common warnings you’ll see in React apps. It usually pops up when you have async code like API calls, timers, or subscriptions that finish after your component has already unmounted.

Don’t worry—this warning is not a crash. It’s React’s way of telling you that you’re trying to update state on a component that no longer exists. In most cases, it doesn’t break your app, but it can cause memory leaks and unexpected behavior. Let’s fix it properly.

What Causes This Warning?

The warning appears when your component triggers a state update after it has been removed from the React tree. This happens most often with:

  • API calls that resolve after the user navigates away
  • setTimeout or setInterval callbacks that fire after unmount
  • Event listeners that aren’t cleaned up
  • Redux or context subscriptions that keep firing

Here’s a typical example that triggers the warning:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data); // Warning if component unmounts before fetch completes
    });
  }, [userId]);

  return <div>{user?.name}</div>;
}

If the UserProfile component unmounts before the fetch finishes, the setUser call will trigger the warning. The component is gone, but React still tries to update its state.

Can’t Perform A React State Update On An Unmounted Component Warning

This specific warning message has been around since React 16. It’s a helpful reminder to clean up your side effects. The warning itself won’t break your app, but ignoring it can lead to memory leaks and hard-to-debug issues.

Why React Shows This Warning

React shows this warning because updating state on an unmounted component is wasteful. The component’s DOM is gone, so the state update does nothing useful. But React still allocates memory for the update, and if you have many such updates, you can leak memory over time.

In React 18 and later, the warning is less aggressive. React’s new concurrent features handle some of these cases automatically. But you should still fix the underlying issue for clean code.

How To Fix The Warning: 5 Proven Methods

Method 1: Use A Cleanup Flag

The simplest fix is to track whether the component is still mounted. Use a ref to store a boolean flag, and check it before updating state.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const mountedRef = useRef(true);

  useEffect(() => {
    return () => {
      mountedRef.current = false;
    };
  }, []);

  useEffect(() => {
    fetchUser(userId).then(data => {
      if (mountedRef.current) {
        setUser(data);
      }
    });
  }, [userId]);

  return <div>{user?.name}</div>;
}

This method works well for simple cases. The cleanup function sets mountedRef.current to false when the component unmounts. The async callback checks the flag before calling setUser.

Method 2: Abort The Async Operation

A better approach is to cancel the async operation itself. For fetch requests, use the AbortController API. This stops the network request entirely, not just the state update.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

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

    fetchUser(userId, { signal: abortController.signal })
      .then(data => {
        setUser(data);
      })
      .catch(error => {
        if (error.name !== 'AbortError') {
          console.error('Fetch failed:', error);
        }
      });

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

  return <div>{user?.name}</div>;
}

When the component unmounts, the cleanup function calls abortController.abort(). This cancels the fetch, and the .catch handler ignores the AbortError. No state update happens because the promise rejects before the .then runs.

Method 3: Use A Custom Hook

If you handle this pattern often, create a custom hook that wraps the cleanup logic. This keeps your components clean and reusable.

function useSafeAsync(asyncFn, deps) {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  const mountedRef = useRef(true);

  useEffect(() => {
    mountedRef.current = true;
    let cancelled = false;

    asyncFn()
      .then(data => {
        if (!cancelled && mountedRef.current) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled && mountedRef.current) {
          setState({ data: null, loading: false, error });
        }
      });

    return () => {
      cancelled = true;
      mountedRef.current = false;
    };
  }, deps);

  return state;
}

Now your component becomes much simpler:

function UserProfile({ userId }) {
  const { data: user, loading, error } = useSafeAsync(
    () => fetchUser(userId),
    [userId]
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

Method 4: Use React Query Or SWR

Data fetching libraries like React Query and SWR handle this warning automatically. They manage component lifecycle and cancel stale requests for you.

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

React Query cancels pending requests when the component unmounts or when the query key changes. This is the most robust solution for data fetching.

Method 5: Use UseEffect Cleanup For Timers And Subscriptions

For timers and event listeners, always clean up in the useEffect return function.

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return <div>Count: {count}</div>;
}

The same pattern applies to event listeners, WebSocket connections, and any subscription-based APIs.

Common Scenarios That Trigger The Warning

Async/Await In UseEffect

Using async functions directly in useEffect is a common source of the warning. React doesn’t support async effects natively, so you need to handle cleanup manually.

// Bad: triggers warning
useEffect(async () => {
  const data = await fetchData();
  setState(data);
}, []);

// Good: use an inner async function
useEffect(() => {
  let cancelled = false;
  
  async function loadData() {
    const data = await fetchData();
    if (!cancelled) {
      setState(data);
    }
  }
  
  loadData();
  
  return () => {
    cancelled = true;
  };
}, []);

Redux Or Context Subscriptions

If you subscribe to Redux or context inside useEffect, make sure to unsubscribe on cleanup.

useEffect(() => {
  const unsubscribe = store.subscribe(() => {
    // handle state change
  });
  
  return () => {
    unsubscribe();
  };
}, []);

React Router Navigation

When users navigate away quickly, pending API calls from the previous page can trigger the warning. Use the AbortController method to cancel these requests.

Should You Ignore This Warning?

In development mode, the warning is noisy but harmless. In production, React doesn’t show the warning at all. However, ignoring it is bad practice because:

  • It indicates potential memory leaks
  • It can cause race conditions in your UI
  • It makes your code harder to debug
  • It can lead to stale state updates

Fix it properly instead of suppressing it. The effort is minimal and your app will be more reliable.

How To Suppress The Warning (Not Recommended)

You can suppress the warning by wrapping setState calls in a try-catch or by using a global override. But don’t do this. Suppressing hides real problems and makes your code fragile.

// Bad practice: don't do this
const originalSetState = useState;
useState = (initial) => {
  const [state, setState] = originalSetState(initial);
  const safeSetState = (value) => {
    try {
      setState(value);
    } catch (e) {
      // silently ignore
    }
  };
  return [state, safeSetState];
};

This approach breaks React’s internal logic and can cause subtle bugs. Always fix the root cause instead.

Best Practices To Avoid The Warning

  1. Always clean up side effects in useEffect return functions
  2. Use AbortController for fetch requests
  3. Check mounted state before updating state in async callbacks
  4. Use data fetching libraries like React Query or SWR
  5. Avoid async functions directly in useEffect
  6. Cancel timers and subscriptions on unmount
  7. Use refs for flags instead of state variables for tracking mount status

Debugging The Warning

When you see the warning in your console, follow these steps:

  1. Open the browser console and note the component stack trace
  2. Identify which async operation triggers the state update
  3. Check if the component has a cleanup function in useEffect
  4. Add a cleanup flag or abort controller
  5. Test by navigating away quickly while the async operation is pending

The stack trace usually points to the exact line where the state update happens. Use that information to trace back to the useEffect that started the async operation.

React 18 And The Warning

React 18 introduced automatic batching and improved concurrent features. In some cases, React 18 suppresses the warning automatically when state updates happen during unmount. But you shouldn’t rely on this behavior. The warning still appears in many scenarios, and fixing it is still best practice.

React 18 also introduced the useSyncExternalStore hook for external subscriptions. This hook handles cleanup automatically and doesn’t trigger the warning.

Advanced: Using UseRef With Cleanup

For complex components with multiple async operations, use a single ref to track mount status across all effects.

function ComplexComponent() {
  const mountedRef = useRef(true);
  const [data1, setData1] = useState(null);
  const [data2, setData2] = useState(null);

  useEffect(() => {
    return () => {
      mountedRef.current = false;
    };
  }, []);

  useEffect(() => {
    fetchData1().then(data => {
      if (mountedRef.current) setData1(data);
    });
  }, []);

  useEffect(() => {
    fetchData2().then(data => {
      if (mountedRef.current) setData2(data);
    });
  }, []);

  // ...
}

This pattern scales well for components with many effects. Just one cleanup function handles all of them.

When The Warning Is Actually A Bug

Sometimes the warning indicates a real bug. For example, if you’re updating state in a callback that should never fire after unmount, you might have a memory leak or an event listener that wasn’t removed.

Common real bugs that cause the warning:

  • Forgotten event listeners on window or document
  • WebSocket connections that aren’t closed
  • setInterval that keeps running after component unmounts
  • Third-party library callbacks that don’t support cleanup

In these cases, fix the root cause by properly cleaning up the subscription or callback.

Testing For The Warning

You can test your components for this warning using React Testing Library. Simulate a component that unmounts while an async operation is pending.

import { render, unmountComponentAtNode } from 'react-dom';

test('does not warn on unmounted component', () => {
  const container = document.createElement('div');
  render(<MyComponent />, container);
  
  // Trigger async operation
  // Immediately unmount
  unmountComponentAtNode(container);
  
  // Wait for async operation to complete
  // No warning should appear in console
});

Use jest.spyOn to catch console.warn calls and assert that the warning doesn’t appear.

Frequently Asked Questions

Does The “Can’t Perform A React State Update On An Unmounted Component” Warning Break My App?

No, the warning doesn’t break your app in most cases. It’s a development-only warning that tells you about a potential issue. However, ignoring it can lead to memory leaks and race conditions over time.

How Do I Fix This Warning In React 18?

Use the same methods as in React 16 and 17: cleanup flags, AbortController, or data fetching libraries. React 18 handles some cases automatically, but you should still fix the underlying issue.

Can I Use Async/await Directly In UseEffect?

No, React doesn’t support async functions directly in useEffect because they return a promise instead of a cleanup function. Always define an inner async function and call it inside the effect.

Why Does This Warning Appear In Production Sometimes?

React suppresses the warning in production builds, but the underlying issue still exists. If you see the warning in production, you’re likely using a development build or a custom logging setup.

Is It Safe To Ignore This Warning If My App Works Fine?

It’s not recommended. The warning indicates that your component has side effects that aren’t properly cleaned up. Over time, these can cause memory leaks and make your app slower.

Conclusion

The “Can’t Perform A React State Update On An Unmounted Component Warning” is a helpful reminder to clean up your side effects. Fixing it is straightforward: use cleanup flags, abort controllers, or data fetching libraries. Don’t suppress the warning—fix the root cause instead.

Start by auditing your components for async operations in useEffect. Add cleanup functions for every effect that starts a timer, makes a network request, or subscribes to an external data source. Your app will be more reliable, and your console will be cleaner.

Remember, this warning is your friend. It helps you write better React code. Embrace it, fix it, and move on to building great user interfaces.