问题
Problem
I am writing an application in React and was unable to avoid a super common pitfall, which is calling setState(...) after componentWillUnmount(...).
I looked very carefully at my code and tried to put some guarding clauses in place, but the problem persisted and I am still observing the warning.
Therefore, I've got two questions:
- How do I figure out from the stack trace, which particular component and event handler or lifecycle hook is responsible for the rule violation?
- Well, how to fix the problem itself, because my code was written with this pitfall in mind and is already trying to prevent it, but some underlying component's still generating the warning.
Browser console
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.
in TextLayerInternal (created by Context.Consumer)
in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29
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>
);
}
Update 1: Cancel throttleable function (still no luck)
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
pdfWrapper: HTMLDivElement | null = null;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
componentDidMount = () => {
this.setDivSizeThrottleable = throttle(
() => {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
},
500,
);
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
window.removeEventListener("resize", this.setDivSizeThrottleable!);
this.setDivSizeThrottleable!.cancel();
this.setDivSizeThrottleable = undefined;
};
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;
回答1:
To remove - Can't perform a React state update on an unmounted component warning, use componentDidMount method under a condition and make false that condition on componentWillUnmount method. For example : -
class Home extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
ajaxVar
.get('https://domain')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
回答2:
try changing setDivSizeThrottleable to
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
回答3:
I had this warning possibly because of calling setState from an effect hook (This is discussed in these 3 issues linked together).
Anyway, upgrading the react version removed the warning.
回答4:
Edit: I just realized the warning is referencing a component called TextLayerInternal. That's likely where your bug is. The rest of this is still relevant, but it might not fix your problem.
1) Getting the instance of a component for this warning is tough. It looks like there is some discussion to improve this in React but there currently is no easy way to do it. The reason it hasn't been built yet, I suspect, is likely because components are expected to be written in such a way that setState after unmount isn't possible no matter what the state of the component is. The problem, as far as the React team is concerned, is always in the Component code and not the Component instance, which is why you get the Component Type name.
That answer might be unsatisfactory, but I think I can fix your problem.
2) Lodashes throttled function has a cancel method. Call cancel in componentWillUnmount and ditch the isComponentMounted. Canceling is more "idiomatically" React than introducing a new property.
来源:https://stackoverflow.com/questions/53949393/cant-perform-a-react-state-update-on-an-unmounted-component