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

485,924

Solution 1

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

Solution 2

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() {
    ...
  }
}

Solution 3

If above solutions dont work, try this and it works for me:

componentWillUnmount() {
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        return;
    };
}

Solution 4

There is a hook that's fairly common called useIsMounted that solves this problem (for functional components)...

import { useRef, useEffect } from 'react';

export function useIsMounted() {
  const isMounted = useRef(false);

  useEffect(() => {
    isMounted.current = true;
    return () => isMounted.current = false;
  }, []);

  return isMounted;
}

then in your functional component

function Book() {
  const isMounted = useIsMounted();
  ...

  useEffect(() => {
    asyncOperation().then(data => {
      if (isMounted.current) { setState(data); }
    })
  });
  ...
}

Solution 5

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.

Share:
485,924
Igor Soloydenko
Author by

Igor Soloydenko

24/7: Loving husband, Dog friend, Critic Software Developer C# and TypeScript code reviewer @ CR.SEm

Updated on July 08, 2022

Comments

  • Igor Soloydenko
    Igor Soloydenko almost 2 years

    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;