Question

Fixing React State Updates on Unmounted Components

typescriptjavascriptreactjslodashsetstate

Question

In a React application, I am seeing the warning: Can't perform a React state update on an unmounted component.

I have already tried to guard against this by tracking whether the component is mounted, but the warning still appears.

I want to understand two things:

  1. How can I use the stack trace to identify which component, event handler, or lifecycle hook is causing the state update after unmount?
  2. How should I properly fix this problem when the code already attempts to avoid it?

Here is the relevant code:

// Book.tsx
import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  setDivSizeThrottleable: () => void;
  pdfWrapper: HTMLDivElement | null = null;
  isComponentMounted: boolean = false;

  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  constructor(props: any) {
    super(props);
    this.setDivSizeThrottleable = throttle(
      () => {
        if (this.isComponentMounted) {
          this.setState({
            pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
          });
        }
      },
      500,
    );
  }

  componentDidMount = () => {
    this.isComponentMounted = true;
    this.setDivSizeThrottleable();
    window.addEventListener('resize', this.setDivSizeThrottleable);
  };

  componentWillUnmount = () => {
    this.isComponentMounted = false;
    window.removeEventListener('resize', this.setDivSizeThrottleable);
  };

  render = () => (
    <div className="Book">
      {this.state.hidden && (
        <div className="Book__LoadNotification centered">Book is being loaded...</div>
      )}

      <div className={this.getPdfContentContainerClassName()}>
        <BookCommandPanel bookTextPath={BookTextPath} />

        <div className="Book__PdfContent" ref={ref => (this.pdfWrapper = ref)}>
          <AutoWidthPdf
            file={BookTextPath}
            width={this.state.pdfWidth}
            onLoadSuccess={(_: any) => this.onDocumentComplete()}
          />
        </div>

        <BookCommandPanel bookTextPath={BookTextPath} />
      </div>
    </div>
  );

  getPdfContentContainerClassName = () =>
    this.state.hidden ? 'hidden' : '';

  onDocumentComplete = () => {
    try {
      this.setState({ hidden: false });
      this.setDivSizeThrottleable();
    } catch (caughtError) {
      console.warn({ caughtError });
    }
  };
}

export default Book;
// AutoWidthPdf.tsx
import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc =
  `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

interface IProps {
  file: string;
  width: number;
  onLoadSuccess: (pdf: any) => void;
}

export class AutoWidthPdf extends React.Component<IProps> {
  render = () => (
    <Document
      file={this.props.file}
      onLoadSuccess={(_: any) => this.props.onLoadSuccess(_) }
    >
      <Page pageNumber={1} width={this.props.width} />
    </Document>
  );
}

I also tried cancelling the throttled function in componentWillUnmount, but the warning still appears.

Short Answer

By the end of this page, you will understand what React means by a state update on an unmounted component, how to read the warning stack trace, why guarding with isMounted often does not solve the real problem, and how to correctly clean up async work, timers, throttled callbacks, event listeners, and third-party callbacks in React.

Concept

React shows this warning when code calls setState after a component has already been removed from the screen.

That usually means one of these is still running after unmount:

  • an async request
  • a timer
  • a throttled or debounced callback
  • an event listener
  • a subscription
  • a callback from a third-party component

Why this matters:

  • React cannot update a component that no longer exists.
  • It often signals a memory leak or unfinished background work.
  • It can cause confusing bugs, especially when navigating between pages or conditionally rendering components.

In the example, there are two likely sources:

  • the throttled resize handler in Book
  • the onLoadSuccess callback from react-pdf / Document

The stack trace is a big clue. This part matters:

in TextLayerInternal (created by Context.Consumer)
in TextLayer (created by PageInternal)
...
_callee$
TextLayer.js:97

That suggests the warning is not necessarily coming from your Book component directly. It may come from an internal component inside react-pdf, likely related to rendering the PDF text layer.

So the real lesson is:

  • sometimes your own component causes the warning
  • sometimes a child library component still has async work running after unmount
  • your cleanup must stop your own callbacks, and sometimes you must also prevent rendering or interaction with the child component once it is no longer valid

A key point: using a boolean like isComponentMounted is only a guard around your own code. It does not cancel the underlying async work. Proper fixes usually involve cleanup, not just conditional checks.

Mental Model

Think of a component like a restaurant table.

  • mount = the customer sits down
  • setState = serving food to that table
  • unmount = the customer leaves

If the kitchen keeps preparing dishes after the customer has left, the waiter eventually tries to deliver food to an empty table. That is the warning.

A boolean like isMounted is like telling the waiter, "Check whether the table is occupied before serving." That can help in some cases.

But the better fix is usually to stop the kitchen work too:

  • cancel the order
  • stop the timer
  • unsubscribe from updates
  • remove the event listener
  • cancel the throttled or debounced callback

In other words:

  • guarding avoids some bad updates
  • cleanup prevents the bad updates from being scheduled in the first place
Take Quiz

Syntax and Examples

The core idea is: if a component starts work, it should also stop that work when it unmounts.

Class component cleanup pattern

class Example extends React.Component {
  timerId?: number;

  state = { message: 'Loading...' };

  componentDidMount() {
    this.timerId = window.setTimeout(() => {
      this.setState({ message: 'Done' });
    }, 1000);
  }

  componentWillUnmount() {
    window.clearTimeout(this.timerId);
  }

  render() {
    return <div>{this.state.message}</div>;
  }
}

Here, the timeout is cleaned up before it can update state after unmount.

Throttle cleanup example

With Lodash throttle, you should cancel the throttled function:

import { throttle } from 'lodash';
import * as React from 'react';

class ResizableBox extends React.Component {
  updateSize = throttle(() => {
    this.setState({ width: window.innerWidth });
  }, 500);

  state = { width: window.innerWidth };

  componentDidMount() {
    window.addEventListener('resize', this.updateSize);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.updateSize);
    this.updateSize.cancel();
  }

  render() {
    return <div>{this.state.width}</div>;
  }
}

Async callback guard example

Sometimes you cannot truly cancel the async work, but you can ignore its result:

class DataLoader extends React.Component {
  _isAlive = false;

  state = { loaded: false };

  componentDidMount() {
    this._isAlive = true;

    fetch('/api/data')
      .then(res => res.json())
      .then(() => {
        if (this._isAlive) {
          this.setState({ loaded: true });
        }
      });
  }

  componentWillUnmount() {
    this._isAlive = false;
  }

  render() {
    return <div>{this.state.loaded ? 'Loaded' : 'Loading...'}</div>;
  }
}

This is better than nothing, but if cancellation is available, cancellation is preferred.

Applying it to the PDF example

Your Book component should clean up:

  • the resize event listener
  • the throttled callback via .cancel()
  • any callback that might run after unmount, such as onLoadSuccess

A safer version looks like this:

import { throttle } from 'lodash';
import * as React from 'react';

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  private isAlive = false;
  private pdfWrapper: HTMLDivElement | null = null;
  private setDivSizeThrottleable = throttle(() => {
    if (!this.isAlive || !this.pdfWrapper) return;

    this.setState({
      pdfWidth: this.pdfWrapper.getBoundingClientRect().width - 5,
    });
  }, 500);

  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  componentDidMount() {
    this.isAlive = true;
    this.setDivSizeThrottleable();
    window.addEventListener('resize', this.setDivSizeThrottleable);
  }

  componentWillUnmount() {
    this.isAlive = false;
    window.removeEventListener('resize', this.setDivSizeThrottleable);
    this.setDivSizeThrottleable.cancel();
  }

  onDocumentComplete = () => {
    if (!this.isAlive) return;

    this.setState({ hidden: false });
    this.setDivSizeThrottleable();
  };

  render() {
    return (
      <div>
        <div ref={ref => (this.pdfWrapper = ref)}>
          <AutoWidthPdf
            file="/file.pdf"
            width={this.state.pdfWidth}
            onLoadSuccess={this.onDocumentComplete}
          />
        </div>
      </div>
    );
  }
}

This does not guarantee that a third-party child library has no internal bug, but it prevents your own component from contributing to the warning.

Step by Step Execution

Consider this example:

class Demo extends React.Component {
  timerId?: number;
  state = { done: false };

  componentDidMount() {
    this.timerId = window.setTimeout(() => {
      this.setState({ done: true });
    }, 2000);
  }

  componentWillUnmount() {
    window.clearTimeout(this.timerId);
  }

  render() {
    return <div>{this.state.done ? 'Done' : 'Waiting'}</div>;
  }
}

What happens step by step

  1. The component mounts.
  2. componentDidMount runs.
  3. A timeout is scheduled for 2 seconds later.
  4. If the component stays mounted, the timeout fires.
  5. setState({ done: true }) runs.
  6. React re-renders the component.

What happens if the component unmounts early

  1. The component mounts.
  2. The timeout is scheduled.
  3. The user navigates away before 2 seconds pass.
  4. componentWillUnmount runs.
  5. clearTimeout cancels the pending timeout.
  6. No later state update happens.

What if cleanup is missing?

Without clearTimeout, step 5 does not happen. The timeout still fires later and tries to call setState on a component that no longer exists. React then shows the warning.

Relating this to your case

Your code has the same shape:

  • a callback is scheduled by throttle
  • a callback may arrive later from react-pdf via onLoadSuccess
  • if the component unmounts before those finish, they may still attempt updates

That is why cleanup and callback guards are both important.

Real World Use Cases

This issue appears often in real React apps.

API requests

A page starts loading user data. The user navigates away before the request finishes. When the response arrives, the old component tries to update state.

Search boxes with debounce or throttle

A search input uses throttled updates to avoid too many requests. If the component disappears, the delayed callback may still fire.

Window or document listeners

Components listening to resize, scroll, or keydown must remove listeners on unmount.

WebSocket or subscription-based apps

Chat apps, dashboards, and live feeds often subscribe to updates. If they forget to unsubscribe, incoming messages try to update removed components.

Third-party UI libraries

Components such as PDF viewers, charts, maps, and media players often perform internal async work. If they are unmounted mid-process, warnings can come from inside the library.

File loading and image processing

Reading files, parsing PDFs, generating previews, or loading media often finishes asynchronously after the user has already closed the view.

Take Quiz

Real Codebase Usage

In real projects, developers usually solve this with a mix of cleanup patterns.

1. Guard clauses

Used when a callback may still resolve after unmount.

if (!this.isAlive) return;

This is useful, but should not be the only strategy.

2. Early cleanup in componentWillUnmount

Typical cleanup list:

  • remove event listeners
  • clear timeouts and intervals
  • cancel throttled or debounced callbacks
  • abort network requests if possible
  • unsubscribe from stores, sockets, or observables

3. Keep references stable

The same function reference used in addEventListener should be used in removeEventListener.

window.addEventListener('resize', this.updateSize);
window.removeEventListener('resize', this.updateSize);

4. Ignore results from stale async work

When true cancellation is not possible, developers mark old work as stale and skip updates.

5. Conditional rendering of async children

If a child library is known to trigger late callbacks, developers sometimes stop rendering it as soon as the screen changes, or wrap the callback so parent updates are ignored after unmount.

6. Investigate library-origin warnings

Your stack trace mentions TextLayerInternal and TextLayer.js, which strongly suggests the warning may originate inside react-pdf or one of its internal components.

In a real codebase, common next steps are:

  • verify the warning reproduces even when parent setState calls are guarded
  • check the library version for known issues
  • search the library issue tracker for the exact warning and stack names
  • isolate the component in a minimal reproduction
  • upgrade or patch the dependency if the problem is internal to the library

That matters because not every warning of this type is caused by the top-level component you wrote.

Common Mistakes

1. Using isMounted as the only fix

Broken idea:

if (this.isMounted) {
  this.setState({ value: 123 });
}

Why it is incomplete:

  • it does not cancel the work
  • the async task still runs
  • child libraries may still update themselves internally

Use it as a guard, but also clean up the source of the callback.

2. Forgetting to cancel throttled or debounced functions

Broken code:

componentWillUnmount() {
  window.removeEventListener('resize', this.handleResize);
}

If handleResize is throttled or debounced, a pending call may still run.

Better:

componentWillUnmount() {
  window.removeEventListener('resize', this.handleResize);
  this.handleResize.cancel();
}

3. Assuming try/catch will stop the warning

Broken idea:

try {
  this.setState({ hidden: false });
} catch (e) {
  console.warn(e);
}

Why this does not help:

  • React's warning is not solved by catching errors
  • this is not usually a thrown exception you should handle with try/catch
  • the real issue is lifecycle cleanup

4. Forgetting null checks for DOM refs

Broken code:

this.pdfWrapper!.getBoundingClientRect().width

If the component is unmounting or the ref is not yet assigned, this can fail.

Safer:

if (!this.pdfWrapper) return;

5. Misreading the stack trace

Beginners often assume the warning always points to their own component. But in your stack, names like TextLayerInternal and TextLayer.js suggest a child library component may be responsible.

6. Removing listeners but leaving other async work active

Removing a resize listener is not enough if you also have:

  • throttled callbacks already queued
  • pending promises
  • library callbacks like onLoadSuccess
  • subscriptions from child components

Comparisons

ApproachWhat it doesGood forLimitation
Guard with boolean (isAlive)Skips setState after unmountSimple async callbacksDoes not cancel underlying work
Cleanup event listenersStops future eventsresize, scroll, DOM eventsDoes not cancel already queued throttled calls
Cancel throttle/debounceRemoves pending delayed callbackLodash throttle / debounceOnly works for those wrapped functions
Clear timeout / intervalStops scheduled timerssetTimeout, setIntervalMust keep timer IDs
Abort requestStops network workfetch with AbortControllerNot all async APIs support cancellation
UnsubscribeStops external updatessockets, observables, storesRequires subscription API support

isMounted flag vs actual cleanup

TechniqueRecommended?Why
Only use isMounted flagUsually noIt hides symptoms but may leave background work running
Cleanup + optional guardYesPrevents work and avoids late updates

Parent bug vs child library bug

SituationClue
Parent component is responsibleStack points to your methods or files
Child library is responsibleStack points to internal library files like TextLayer.js

In your case, the stack trace strongly hints that react-pdf internals may be involved, not just Book.tsx.

Take Quiz

Cheat Sheet

Quick rules

  • Do not call setState after unmount.
  • Clean up everything started in componentDidMount.
  • Cancel throttled and debounced callbacks.
  • Remove event listeners.
  • Clear timers.
  • Unsubscribe from subscriptions.
  • Abort async work when possible.
  • Guard callbacks when cancellation is not possible.

Class component cleanup template

class MyComponent extends React.Component {
  private isAlive = false;

  componentDidMount() {
    this.isAlive = true;
    // start work here
  }

  componentWillUnmount() {
    this.isAlive = false;
    // stop work here
  }
}

Throttle pattern

handler = throttle(() => {
  if (!this.isAlive) return;
  this.setState({ value: 1 });
}, 300);

componentWillUnmount() {
  this.handler.cancel();
}

Event listener pattern

componentDidMount() {
  window.addEventListener('resize', this.handler);
}

componentWillUnmount() {
  window.removeEventListener('resize', this.handler);
}

Timer pattern

timerId = window.setTimeout(() => {
  if (!this.isAlive) return;
  this.setState({ done: true });
}, 1000);

componentWillUnmount() {
  window.clearTimeout(this.timerId);
}

How to inspect the warning

  • Read the first component names in the warning.
  • Look for your file names in the stack.
  • If the stack points into a dependency, test with a minimal reproduction.
  • Check the dependency's issue tracker and version history.

Important edge case

A removed event listener does not cancel a pending throttled call. Use both:

window.removeEventListener('resize', this.handler);
this.handler.cancel();

FAQ

What does "Can't perform a React state update on an unmounted component" mean?

It means some code called setState after the component had already been removed from the UI.

Does this always mean I have a memory leak?

Not always a severe leak, but it does mean some async work or subscription outlived the component and should be cleaned up.

Why didn't my isMounted check fully solve it?

Because it only guards your own setState calls. It does not stop timers, throttled callbacks, requests, or child library internals from continuing.

How do I know which component caused the warning?

Look at the stack trace and component names. If the stack points to library files such as TextLayer.js, the issue may come from a child dependency rather than your own component.

Should I use try/catch around setState?

No. That does not fix the lifecycle problem. You should clean up the work that leads to the update.

How do I fix throttled or debounced callbacks in React?

Remove any related listeners and call .cancel() on the throttled or debounced function in unmount cleanup.

What if the warning comes from a third-party library?

Create a minimal reproduction, check for updates, search the library issue tracker, and confirm whether the problem is internal to that dependency.

Is this different in function components?

The idea is the same. In function components, cleanup is usually done in the function returned from useEffect.

Take Quiz

Mini Project

Description

Build a small React component that listens for window resizing and simulates loading data asynchronously. The goal is to practice proper cleanup so the component never tries to update state after unmounting.

Goal

Create a class component that safely handles both a timer and a throttled resize listener without triggering unmounted-component warnings.

Requirements

  • Create a class component with local state for width and loaded
  • Add a throttled resize handler that updates the width in state
  • Start an async operation with setTimeout that marks the component as loaded
  • Clean up the resize listener, throttled callback, and timeout in componentWillUnmount
  • Prevent late callbacks from calling setState after unmount
Take Quiz

Keep learning

Related questions

View more