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:
- How can I use the stack trace to identify which component, event handler, or lifecycle hook is causing the state update after unmount?
- 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
onLoadSuccesscallback fromreact-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
Mental Model
Think of a component like a restaurant table.
mount= the customer sits downsetState= serving food to that tableunmount= 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
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:
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
- The component mounts.
componentDidMountruns.- A timeout is scheduled for 2 seconds later.
- If the component stays mounted, the timeout fires.
setState({ done: true })runs.
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.
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.
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', .);
..();
}
Comparisons
| Approach | What it does | Good for | Limitation |
|---|---|---|---|
Guard with boolean (isAlive) | Skips setState after unmount | Simple async callbacks | Does not cancel underlying work |
| Cleanup event listeners | Stops future events | resize, scroll, DOM events | Does not cancel already queued throttled calls |
| Cancel throttle/debounce | Removes pending delayed callback | Lodash throttle / debounce | Only works for those wrapped functions |
| Clear timeout / interval | Stops scheduled timers | setTimeout, |
Cheat Sheet
Quick rules
- Do not call
setStateafter 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.({ : });
}, );
() {
..();
}
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?
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
widthandloaded - Add a throttled resize handler that updates the width in state
- Start an async operation with
setTimeoutthat marks the component as loaded - Clean up the resize listener, throttled callback, and timeout in
componentWillUnmount - Prevent late callbacks from calling
setStateafter unmount
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular @ViewChild Returning Undefined: Lifecycle, Child Components, and Fixes
Learn why Angular @ViewChild can be undefined, when it becomes available, and how to access child components correctly using lifecycle hooks.