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:
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
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> ); }
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;
The warning "Can't perform a React state update on an unmounted component" is caused when we try to update the state of an unmounted component. A straight forward way to get rid of the warning is to keep track of whether the component is mounted using an isMounted boolean in our useEffect hook.
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 a useEffect cleanup function.
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 a useEffect cleanup function.
One should never update the state directly because of the following reasons: If you update it directly, calling the setState() afterward may just replace the update you made. When you directly update the state, it does not change this.
Here is a React Hooks specific solution for
Warning: Can't perform a React state update on an unmounted component.
You can declare let isMounted = true
inside useEffect
, which will be changed in the cleanup callback, as soon as the component is unmounted. Before state updates, you now check this variable conditionally:
useEffect(() => { let isMounted = true; // note mutable flag someAsyncOperation().then(data => { if (isMounted) setState(data); // add conditional check }) return () => { isMounted = false }; // cleanup toggles value, if unmounted }, []); // adjust dependencies to your needs
const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useEffect(() => { let isMounted = true; fetchData(); return () => { isMounted = false; }; // simulate some Web API fetching function fetchData() { setTimeout(() => { // drop "if (isMounted)" to trigger error again // (take IDE, doesn't work with stack snippet) if (isMounted) setState("data fetched") else console.log("aborted setState on unmounted component") }, 4000); } }, []); return <div>Child: {state}</div>; }; ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
useAsync
HookWe can encapsulate all the boilerplate into a custom Hook, that automatically aborts async functions in case the component unmounts or dependency values have changed before:
function useAsync(asyncFn, onSuccess) { useEffect(() => { let isActive = true; asyncFn().then(data => { if (isActive) onSuccess(data); }); return () => { isActive = false }; }, [asyncFn, onSuccess]); }
// custom Hook for automatic abortion on unmount or dependency change // You might add onFailure for promise errors as well. function useAsync(asyncFn, onSuccess) { useEffect(() => { let isActive = true; asyncFn().then(data => { if (isActive) onSuccess(data) else console.log("aborted setState on unmounted component") }); return () => { isActive = false; }; }, [asyncFn, onSuccess]); } const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useAsync(simulateFetchData, setState); return <div>Child: {state}</div>; }; const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const simulateFetchData = () => new Promise( resolve => setTimeout(() => resolve("data fetched"), 4000)); ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
More on effect cleanups: Overreacted: A Complete Guide to useEffect
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With