Can’t Perform a React State Update on an Unmounted Component

A common problem in a React dashboard happens when a user opens a customer details page, starts loading data, and quickly navigates away. The API request finishes afterward, and the old component still tries to update its state.

I have seen this issue often in real-world React applications with API calls, timers, subscriptions, and delayed actions. The key is understanding when a component leaves the screen and cleaning up work that should no longer continue.

Let’s look at why this warning happens and the best ways to prevent React state updates after a component unmounts.

What Does an Unmounted Component Mean in React?

A React component is a reusable piece of JavaScript that returns UI using JSX. React mounts a component when it adds the component to the page.

React unmounts a component when it removes that component from the page.

For example, imagine a customer support dashboard for a company in Austin, Texas:

  • A user opens a customer profile.
  • React mounts the CustomerDetails component.
  • The component starts an asynchronous operation.
  • The user closes the profile before the operation finishes.
  • React unmounts the component.
  • The asynchronous operation finishes and tries to update state.

That final step causes the problem.

The warning often appears as:

Can't perform a React state update on an unmounted component.

In modern React versions, you may not always see this exact warning, but updating application logic after cleanup remains a real problem. You should still cancel requests, timers, and subscriptions when they are no longer needed.

Why Does a React State Update on an Unmounted Component Happen?

State stores data that can change while users interact with a component.

A state update usually looks like this:

setCustomer(customerData);

The problem occurs when setCustomer() runs after React has already removed the component.

The most common causes include:

  • API requests
  • setTimeout()
  • setInterval()
  • WebSocket connections
  • Event listeners
  • Subscriptions
  • Promises that finish after navigation

This issue commonly appears in applications that load customer records, reports, or dashboard data. If you are building components with changing state, you may also find React component state update troubleshooting useful.

React State Update on an Unmounted Component With an API Request

Let’s start with a simple example.

The component below loads customer data after mounting.

App.jsx

import { useEffect, useState } from "react";

function CustomerDetails() {
  const [customer, setCustomer] = useState(null);

  useEffect(() => {
    async function loadCustomer() {
      await new Promise((resolve) => setTimeout(resolve, 3000));

      setCustomer({
        name: "Emily Carter",
        location: "Seattle, Washington"
      });
    }

    loadCustomer();
  }, []);

  return (
    <div>
      <h2>Customer Details</h2>

      {customer ? (
        <p>
          {customer.name} from {customer.location}
        </p>
      ) : (
        <p>Loading customer...</p>
      )}
    </div>
  );
}

function App() {
  const [showCustomer, setShowCustomer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowCustomer(!showCustomer)}>
        {showCustomer ? "Hide Customer" : "Show Customer"}
      </button>

      {showCustomer && <CustomerDetails />}
    </div>
  );
}

export default App;

Sample output

Initially, the browser displays:

Customer Details

Loading customer...

Hide Customer

You can refer to the screenshot below to see the output.

Can’t Perform a React State Update on Unmounted Component

If you click Hide Customer before three seconds finish, React removes CustomerDetails.

However, the delayed operation still finishes and calls:

setCustomer(...)

The component no longer exists in the UI.

Why this happens

The useEffect hook lets a functional component perform side effects. A side effect includes work outside normal rendering, such as loading data or starting a timer.

The asynchronous operation continues independently. Removing the component does not automatically stop every operation that JavaScript already started.

This is why cleanup matters.

Fix React State Update on an Unmounted Component With Cleanup

The useEffect hook supports a cleanup function.

React runs the cleanup when the component unmounts or before the effect runs again when dependencies change.

Here is a safer version using clearTimeout().

App.jsx

import { useEffect, useState } from "react";

function CustomerDetails() {
  const [customer, setCustomer] = useState(null);

  useEffect(() => {
    const timerId = setTimeout(() => {
      setCustomer({
        name: "Emily Carter",
        location: "Seattle, Washington"
      });
    }, 3000);

    return () => {
      clearTimeout(timerId);
    };
  }, []);

  return (
    <div>
      <h2>Customer Details</h2>

      {customer ? (
        <p>
          {customer.name} from {customer.location}
        </p>
      ) : (
        <p>Loading customer...</p>
      )}
    </div>
  );
}

function App() {
  const [showCustomer, setShowCustomer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowCustomer(!showCustomer)}>
        {showCustomer ? "Hide Customer" : "Show Customer"}
      </button>

      {showCustomer && <CustomerDetails />}
    </div>
  );
}

export default App;

Sample output

Initially:

Customer Details

Loading customer...

Hide Customer

If you wait three seconds:

Customer Details

Emily Carter from Seattle, Washington

Hide Customer

You can refer to the screenshot below to see the output.

Can’t Perform a React State Update on an Unmounted Component

If you click Hide Customer before three seconds finish, the cleanup function runs and cancels the timer.

Why this solution works

The code stores the timer ID:

const timerId = setTimeout(...)

The cleanup function then runs:

return () => {
  clearTimeout(timerId);
};

React calls this cleanup when it removes the component. This prevents the delayed state update.

Pro Tip: I always clean up timers and subscriptions immediately when I create them. Waiting until a component starts causing bugs makes asynchronous React code much harder to debug.

Fix API Requests With AbortController

Timers are easy to cancel with clearTimeout(). API requests need a different approach. For browser fetch() requests, AbortController provides a clean way to cancel an active request.

App.jsx

import { useEffect, useState } from "react";

function CustomerDetails() {
  const [customer, setCustomer] = useState(null);
  const [error, setError] = useState("");

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

    async function loadCustomer() {
      try {
        const response = await fetch(
          "/api/customers/emily-carter",
          {
            signal: controller.signal
          }
        );

        if (!response.ok) {
          throw new Error("Unable to load customer details.");
        }

        const customerData = await response.json();

        setCustomer(customerData);
      } catch (error) {
        if (error.name !== "AbortError") {
          setError(error.message);
        }
      }
    }

    loadCustomer();

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

  return (
    <div>
      <h2>Customer Details</h2>

      {error && <p>{error}</p>}

      {!customer && !error && <p>Loading customer...</p>}

      {customer && (
        <p>
          {customer.name} from {customer.location}
        </p>
      )}
    </div>
  );
}

function App() {
  const [showCustomer, setShowCustomer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowCustomer(!showCustomer)}>
        {showCustomer ? "Close Customer" : "Open Customer"}
      </button>

      {showCustomer && <CustomerDetails />}
    </div>
  );
}

export default App;

Sample output

While the request runs:

Customer Details

Loading customer...

Close Customer

After the API responds successfully:

Customer Details

Emily Carter from Seattle, Washington

Close Customer

If the user closes the customer panel before the request finishes, this code runs:

controller.abort();

The request stops instead of continuing unnecessarily.

Why AbortController is useful

AbortController solves two problems:

  1. It stops unnecessary network activity.
  2. It prevents your component from processing a result after users leave the page.

For real API-driven applications, I prefer cancellation over simply ignoring the final result. If you work with asynchronous rendering patterns, using await in React components can provide additional related context.

Clean Up setInterval to Prevent Unmounted Component Problems

A setInterval continues running until JavaScript clears it. Consider a sales dashboard that refreshes information every five seconds.

App.jsx

import { useEffect, useState } from "react";

function SalesDashboard() {
  const [refreshCount, setRefreshCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setRefreshCount((currentCount) => currentCount + 1);
    }, 5000);

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

  return (
    <div>
      <h2>Austin Sales Dashboard</h2>
      <p>Dashboard refreshed: {refreshCount} times</p>
    </div>
  );
}

function App() {
  const [showDashboard, setShowDashboard] = useState(true);

  return (
    <div>
      <button onClick={() => setShowDashboard(!showDashboard)}>
        {showDashboard ? "Close Dashboard" : "Open Dashboard"}
      </button>

      {showDashboard && <SalesDashboard />}
    </div>
  );
}

export default App;

Sample output

Initially:

Austin Sales Dashboard

Dashboard refreshed: 0 times

You can refer to the screenshot below to see the output.

Can’t Perform a React State Update on a Unmounted Component

After five seconds:

Austin Sales Dashboard

Dashboard refreshed: 1 times

After ten seconds:

Austin Sales Dashboard

Dashboard refreshed: 2 times