Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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:

  1. How do I figure out from the stack trace, which particular component and event handler or lifecycle hook is responsible for the rule violation?
  2. 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 

enter image description here

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; 
like image 601
Igor Soloydenko Avatar asked Dec 27 '18 18:12

Igor Soloydenko


People also ask

Can perform a React state update on an unmounted component?

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.

How would you solve can't perform a React state update on an unmounted component?

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.

How do you fix 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?

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.

Why can't we update the state directly in React?

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.


1 Answers

Here is a React Hooks specific solution for

Error

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

Solution

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>

Extension: Custom useAsync Hook

We 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

like image 190
ford04 Avatar answered Oct 14 '22 12:10

ford04