419

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;
Igor Soloydenko
  • 11,067
  • 11
  • 47
  • 90

33 Answers33

464

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

ford04
  • 66,267
  • 20
  • 199
  • 171
  • 7
    We leverage the built-in effect [cleanup](https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1) feature here, which runs when dependencies change and in either case when the component unmounts. So this is the perfect place to toggle an `isMounted` flag to `false`, which can be accessed from the surrounding effect callback closure scope. You can think of the cleanup function as [belonging to](https://overreacted.io/a-complete-guide-to-useeffect/#so-what-about-cleanup) its corresponding effect. – ford04 Jul 02 '20 at 23:28
  • holy smokes...this `isMounted` thing works. I am on react-testing-lib `10.4.7`, and formik `^2.1.4`. This feels like a total hack and a result of something with Formik. – Phil Lucks Jul 17 '20 at 15:27
  • @PhilLucks glad it works. This pattern is a very common and simple way to abort async functions, no hack involved (if I am not mistaken, a similar approach is even recommended in the Hooks FAQ). – ford04 Jul 17 '20 at 15:48
  • So, should we wrap all of our state updates in a if(isMounted){ /* state update here */ } ?? – Victor Molina Nov 04 '20 at 06:51
  • 1
    @VictorMolina No, that certainly would be overkill. Consider this technique for components a) using asynchronous operations like `fetch` in `useEffect` and b) that are not stable, i.e. might be unmounted before the async result returns and is ready to be set as state. – ford04 Nov 04 '20 at 06:59
  • Oh! By adding an `await` on my `async` functions it works as expected! – Alexis Wilke Dec 24 '20 at 18:34
  • @AlexisWilke yes, you can define an inner `async` function inside the `useEffect` callback body [as shown in this post](https://stackoverflow.com/questions/53332321/react-hook-warnings-for-async-function-in-useeffect-useeffect-function-must-ret). This way, you can use `await` instead of `myPromise.then(...)`. – ford04 Dec 24 '20 at 22:14
  • 6
    https://stackoverflow.com/a/63213676/ and https://medium.com/better-programming/why-cant-my-component-unmount-in-react-fd2c13cd58f4 were interesting but ultimately your answer is what finally helped me get mine working. Thanks! – Ryan Jan 06 '21 at 23:30
  • @Goran_Ilic_Ilke we don't do that. `return () => { isMounted = false };` means: return a zero-param [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions), whose return type is `void`, as inside we are applying the side effect of toggling the `isMounted` state variable. I guess you have mixed it up with `return () => ({ isMounted = false });`, see above link. – ford04 Feb 21 '21 at 10:25
  • 3
    This seems like a nice solution if you've already narrowed down the cause/useEffect that is causing the problem, but one of the questions 1. asks how to figure out which component, handler, hook etc is responsible for this error. Why are there so many upvotes on a problem that doesn't make any mention of the whole problem and answer both questions? I checked and there also aren't any posts in this entire thread about any tips to go about finding the source of the error; is it impossible to figure out based on the stack trace? – Jonathan Jun 18 '21 at 04:25
  • Note that the custom `useAsync` hook will fire on each render unless you wrap the callbacks in a `useCallback` – Woodz Jul 02 '21 at 12:30
  • 3
    @Woodz yes, good hint. `useCallback` is the usual and recommended way in React to defer responsibility for dependencies to the client of `useAsync`. You might switch to mutable refs inside `useAsync` to store the most recent callback, so clients can directly pass their functions/callbacks without dependencies. But I would sparingly use this pattern, as probably more confusing and imperative approach. – ford04 Jul 03 '21 at 09:55
  • Question, if I just ignore it can it harm my app? I haven't see it damaging or messing up my app so far. – ReactPotato Nov 17 '21 at 06:40
  • @ford04 May I know why `setTimeout()` is necessary for the first solution to work? Why can't I just `setState` if `isMounted` is true, without delaying it for 4s ? – Tahi Reu Feb 24 '22 at 19:22
  • [isMounted is an antipattern](https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html), isn't it? – CheddarLizzard Mar 02 '22 at 11:32
  • @TahiReu the timeout is just for illustration in context of this example and meant to give you some time to unmount the component, before set is invoked. Feel free to suggest improvements! – ford04 Mar 10 '22 at 09:57
  • 1
    @Theophany I guess the article writes about the old `isMounted()` method in classes. In fact it suggest to create `_isMounted` property, which is what we are doing here for Hooks. You can also find a related example in the [FAQ](https://reactjs.org/docs/hooks-faq.html#how-can-i-do-data-fetching-with-hooks). A viable alternative for HTTP requests is to cancel the fetch itself e.g. via `AbortController`. – ford04 Mar 10 '22 at 10:00
  • Can you show an example of using `await`? – Eduards Mar 20 '22 at 19:24
  • 1
    @EdGzi take a look at https://stackoverflow.com/questions/53332321/react-hook-warnings-for-async-function-in-useeffect-useeffect-function-must-ret as an example. Just make sure, you don't annotate `async` directly at the useEffect callback – ford04 May 12 '22 at 16:43
  • Just a line like `if (!something) return;` fixed my issue. – Manohar Reddy Poreddy Sep 20 '22 at 15:04
145

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() {
    ...
  }
}
vinod
  • 2,850
  • 1
  • 18
  • 23
  • 8
    This worked, but why should this work? What exactly causes this error? and how this fixed it :| – Abhinav Feb 22 '20 at 19:55
  • 2
    It work fine. It stops the repetitive call of setState method because it validate _isMounted value before setState call, then at last again reset to false in componentWillUnmount(). I think, that's the way it work. – Abhishek Mar 02 '20 at 10:51
  • 11
    for hook component use this: `const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });` – x-magix May 22 '20 at 13:37
  • 1
    @x-magix You don't really need a ref for this, you can just use a local variable which the return function can close on. – Mordechai Jun 15 '20 at 21:25
  • 3
    @Abhinav My best guess why this works is that `_isMounted` isn't managed by React (unlike `state`) and is therefore not subject to React's [rendering pipeline](https://reactjs.org/docs/react-component.html#setstate). The issue is that when a component is set to be unmounted, React dequeues any calls to `setState()` (which would trigger a 're-render'); therefore, the state is never updated – Lightfire228 Oct 15 '20 at 22:35
  • If you use Babel and have enabled experimental JavaScript proposals then you can use a [private class field](https://github.com/tc39/proposal-private-methods) for the flag, i.e. `this.#isMounted` – Stefan Becker Jan 27 '21 at 09:16
  • @Mordechai I believe it was done to avoid following warning from compiler: ``` Assignments to the 'isMounted' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect ``` – havish Jun 01 '21 at 06:50
  • You won't get this warning of the variable is declared locally in the useEffect and an empty dependency array. – Mordechai Jun 01 '21 at 13:13
89

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;
    };
}
May'Habit
  • 1,334
  • 12
  • 18
  • 2
    @BadriPaudel return null when escapse component, it will no longer hold any data in memory – May'Habit Jun 23 '20 at 14:49
  • 2
    return what? just paste it like it is? – plus Nov 01 '20 at 13:39
  • 4
    I don't recommend this solution, it's pretty hacky. @BadriPaudel This will replace the setState function after the componentWillUnmount with a function that does nothing. The setState function will continue to be called. – Cramer Gabriel Jul 12 '21 at 13:55
  • This is a great work around. Yes its a bit of a hack, but in the app I am working on theres no obvious reason this error is being thrown. This overrides setState on unmount until the component gets mounted again. I don't see anything wrong with it. – Nik Hammer-Ellis Mar 30 '23 at 18:20
54

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); }
    })
  });
  ...
}
sfletche
  • 47,248
  • 30
  • 103
  • 119
  • 1
    Can we use the same hook with multiple component? – Ayush Kumar Aug 27 '21 at 04:24
  • 5
    @AyushKumar: yes you can! that's the beauty of hooks! the `isMounted` state will be specific to each component that calls `useIsMounted`! – sfletche Sep 04 '21 at 04:43
  • 1
    I guess this way of `useIsMounted` solving should be included in the core package. – dhanushkac Nov 02 '21 at 20:23
  • Another question is if I have added UseIsMounted, in my useEffect hook, and I have started a **listener**. Will adding a ```return () =>``` inside of code will cause any leak? – Ayush Kumar Nov 16 '21 at 16:45
22

Checking if a component is mounted is actually an anti pattern as per React documentation. The solution to the setState warning is rather to leverage on the use of an AbortController:

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort()                       // stop the query by aborting on the AbortController on unmount
  }
}, [])

For asynchronous operations that aren't based on the Fetch API, there still should be a way to cancel these asynchronous operations, and you should rather leverage these than just checking if a component is mounted. If you are building your own API, you can implement the AbortController API in it to handle it.

For more context, the check if a component is mounted is an anti pattern as React is checking internally if the component is mounted to display that warning. Doing the same check again is just a way to hide the warning, and there are some easier ways to hide them than adding this piece of code on a big part of a codebase.

Source: https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934

Matthieu Kern
  • 241
  • 2
  • 4
  • 1
    Yes, this is the correct solution: stopping async operations. Everything based on `isMounted` logic is wrong and was [deprecated by React in 2015;](https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html) reimplementing it defeats the purpose of the warning. – fregante Oct 06 '22 at 05:53
  • 1
    FINALLY. God, I was going nuts sifting trough top answers. This is the way. – DanteTheSmith Mar 24 '23 at 10:38
16

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.

Peter Lamberg
  • 8,151
  • 3
  • 55
  • 69
10

React already removed this warning but here is a better solution (not just workaround)

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort() 
  }
}, [])
scr2em
  • 974
  • 8
  • 17
  • Great solution when you fetch something! Much better to stop fetching (saves data for user + server resources) – Lars Flieger Mar 09 '22 at 10:30
  • -1. This answer is just a worse version of an [earlier answer](https://stackoverflow.com/a/69484869/8285811) that also doesn't link the [source](https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934). – Akaisteph7 May 31 '23 at 15:34
8

The solution from @ford04 didn't worked to me and specially if you need to use the isMounted in multiple places (multiple useEffect for instance), it's recommended to useRef, as bellow:

  1. Essential packages
"dependencies": 
{
  "react": "17.0.1",
}
"devDependencies": { 
  "typescript": "4.1.5",
}

  1. My Hook Component
export const SubscriptionsView: React.FC = () => {
  const [data, setData] = useState<Subscription[]>();
  const isMounted = React.useRef(true);

  React.useEffect(() => {
    if (isMounted.current) {
      // fetch data
      // setData (fetch result)

      return () => {
        isMounted.current = false;
      };
    }
  }
});
Daniel Santana
  • 1,493
  • 20
  • 19
  • 1
    Agree on the point and it is more convenient to go with this solution and it ensures single source of truth. – dhanushkac Nov 02 '21 at 20:20
5

try changing setDivSizeThrottleable to

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);
ic3b3rg
  • 14,629
  • 4
  • 30
  • 53
  • I did try it. Now I am consistently seeing the warning which I was only observing time to time on resizing the window before making this change. ¯\_(ツ)_/¯ Thanks for trying this though. – Igor Soloydenko Dec 28 '18 at 01:09
5

I know that you're not using history, but in my case I was using the useHistory hook from React Router DOM, which unmounts the component before the state is persisted in my React Context Provider.

To fix this problem I have used the hook withRouter nesting the component, in my case export default withRouter(Login), and inside the component const Login = props => { ...; props.history.push("/dashboard"); .... I have also removed the other props.history.push from the component, e.g, if(authorization.token) return props.history.push('/dashboard') because this causes a loop, because the authorization state.

An alternative to push a new item to history.

5

Add a ref to a jsx component and then check it exist

function Book() {
  const ref = useRef();

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

  return <div ref={ref}>content</div>
}
Scott Wager
  • 758
  • 11
  • 9
4

I had a similar issue thanks @ford04 helped me out.

However, another error occurred.

NB. I am using ReactJS hooks

ndex.js:1 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

What causes the error?

import {useHistory} from 'react-router-dom'

const History = useHistory()
if (true) {
  history.push('/new-route');
}
return (
  <>
    <render component />
  </>
)

This could not work because despite you are redirecting to new page all state and props are being manipulated on the dom or simply rendering to the previous page did not stop.

What solution I found

import {Redirect} from 'react-router-dom'

if (true) {
  return <redirect to="/new-route" />
}
return (
  <>
    <render component />
  </>
)
Niyongabo Eric
  • 1,333
  • 18
  • 21
4

If you are fetching data from axios and the error still occurs, just wrap the setter inside the condition

let isRendered = useRef(false);
useEffect(() => {
    isRendered = true;
    axios
        .get("/sample/api")
        .then(res => {
            if (isRendered) {
                setState(res.data);
            }
            return null;
        })
        .catch(err => console.log(err));
    return () => {
        isRendered = false;
    };
}, []);
Drew Cordano
  • 962
  • 9
  • 16
4

I have 2 solutions for this error:

  1. return:

If you are used hook and useEffect, So put a return end of useEffect.

useEffect(() => {
    window.addEventListener('mousemove', logMouseMove)
    return () => {
        window.removeEventListener('mousemove', logMouseMove)
    }
}, [])
  1. componentWillUnmount:

If you are used componentDidMount, So put componentWillUnmount next to it.

componentDidMount() { 
    window.addEventListener('mousemove', this.logMouseMove)
}

componentWillUnmount() {
    window.removeEventListener('mousemove', this.logMouseMove)
}
ParisaN
  • 1,816
  • 2
  • 23
  • 55
3

The isMounted approach is an anti-pattern in most cases because it doesn't actually clean up/cancel anything, it just avoids changing state on unmounted components, but does nothing with pending asynchronous tasks. The React team recently removed the leak warning because users keep creating a lot of anti-patterns to hide the warning rather than fix its cause.

But writing cancellable code in plain JS can be really tricky. To fix this I made my own lib useAsyncEffect2 with custom hooks, built on top of a cancellable promise (c-promise2) for executing cancellable async code to reach its graceful cancellation. All async stages (promises), including deep ones, are cancellable. This means that the request here will be automatically aborted if its parent context is canceled. Of course, any other asynchronous operation can be used instead of a request.

  • useAsyncEffect Demo with plain useState usage (Live Demo):
    import React, { useState } from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({url}) {
      const [text, setText] = useState("");
    
      const cancel = useAsyncEffect(
        function* () {
          setText("fetching...");
          const json = (yield cpAxios(url)).data;
          setText(`Success: ${JSON.stringify(json)}`);
        },
        [url]
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={cancel}>
            Cancel request
          </button>
        </div>
      );
    }
  • useAsyncEffect Demo with internal states usage (Live Demo):
    import React from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({ url, timeout }) {
      const [cancel, done, result, err] = useAsyncEffect(
        function* () {
          return (yield cpAxios(url).timeout(timeout)).data;
        },
        { states: true, deps: [url] }
      );
    
      return (
        <div>
          {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
          <button onClick={cancel} disabled={done}>
            Cancel async effect (abort request)
          </button>
        </div>
      );
    }
  • Class component using decorators (Live demo)
import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component {
  state = {
    text: ""
  };

  *componentDidMount(scope) {
    const { url, timeout } = this.props;
    const response = yield cpAxios(url).timeout(timeout);
    this.setState({ text: JSON.stringify(response.data, null, 2) });
  }

  render() {
    return (<div>{this.state.text}</div>);
  }
}

export default TestComponent;

More other examples:

Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7
2

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.

R Esmond
  • 1,224
  • 9
  • 17
  • Issue is, I don't directly control `TextLayerInternal`. Thus, I don't know "who's fault is the `setState()` call". I'll try the `cancel` as per your advice and see how it goes, – Igor Soloydenko Dec 27 '18 at 20:29
  • Unfortunately, I still see the warning. Please check the code in Update 1 section to verify I'm doing things the right way. – Igor Soloydenko Dec 27 '18 at 23:37
2

UPDATE DO NOT USE MY ORIGINAL ANSWER AS IT DOES NOT WORK

This answer was based on the use of cancelable promises and a note in makecancelable which I migrated to use hooks. However, it appears it does not cancel a chain of async/await and even cancelable-promise does not support canceling of a chain of awaits

Doing a bit more research on this, it appears that some internal Google reasons prevented cancelable promises from coming into the standard.

Further more, there was some promise with Bluebird which introduces cancelable promises, but it does not work in Expo or at least I haven't seen an example of it working in Expo.

The accepted answer is the best. Since I use TypeScript I had adapted the code with a few modifications (I explicitly set the dependencies since the accepted answer's implicit dependencies appear to give a re-render loop on my app, added and use async/await rather than promise chain, pass a ref to the mounted object so that an async/await chain can be canceled earlier if needed)

/**
 * This starts an async function and executes another function that performs
 * React state changes if the component is still mounted after the async
 * operation completes
 * @template T
 * @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
 *   it has a copy of the mounted ref so an await chain can be canceled earlier.
 * @param {(asyncResult: T) => void} onSuccess this gets executed after async
 *   function is resolved and the component is still mounted
 * @param {import("react").DependencyList} deps
 */
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    (async () => {
      const x = await asyncFunction(mountedRef);
      if (mountedRef.current) {
        onSuccess(x);
      }
    })();
    return () => {
      mountedRef.current = false;
    };
  }, deps);
}

Original answer

Since I have many different operations that are async, I use the cancelable-promise package to resolve this issue with minimal code changes.

Previous code:

useEffect(() => 
  (async () => {
    const bar = await fooAsync();
    setSomeState(bar);
  })(),
  []
);

New code:

import { cancelable } from "cancelable-promise";

...

useEffect(
  () => {
    const cancelablePromise = cancelable(async () => {
      const bar = await fooAsync();
      setSomeState(bar);
    })
    return () => cancelablePromise.cancel();
  },
  []
);

You can alsowrpte it in a custom utility function like this

/**
 * This wraps an async function in a cancelable promise
 * @param {() => PromiseLike<void>} asyncFunction
 * @param {React.DependencyList} deps
 */
export function useCancelableEffect(asyncFunction, deps) {
  useEffect(() => {
    const cancelablePromise = cancelable(asyncFunction());
    return () => cancelablePromise.cancel();
  }, deps);
}
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265
2

In my case of a login-like screen, the fetch was done in a onClick handler of a parent component, who passed that handler down to the child, whom placed .catch and .finally on it.

In the .then case a redirect (and hence unmount) would happen as normal operation, and only in cases of fetch error would the child stay mounted on-screen.

My solution was moving the setState and all other code from the .finally to the .catch since the child is guaranteed to be mounted in the .catch case. And in the .then case nothing needed doing because of the guaranteed unmount.

Ron Newcomb
  • 2,886
  • 21
  • 24
2

I have sifted trough the answers on this fairly popular question and now I get where my juniors and mids all got the same idea how to handle this problem.

Firstly it needs to be said this answer relies on "post hooks" react. I tend to enforce that style over the old one because more readability and less boilerplate. If you prefer the React components over functional ones, I am not gonna argue, you do you, I will focus on the functional components/hooks solution.

Sadly I have to say that the "You can declare let isMounted = true inside useEffect, which will be changed in the cleanup and all the similar answers offering some variance of isMounted" is in fact wrong. It works but not in a way one might expect.

It simply MOVES the check that React is ALREADY DOING before the place in timeline when React does it. That is it. The result is React now not showing you a warning. This is actually an ANTIPATTERN.

So what, warning got hidden - no harm no foul?

You have invested time, you have expanded codebase, that IS SOMETHING and achieved nothing. Well, not entirely nothing, WORSE that nothing, you have removed legitimate warning telling you to optimize your code and effectively scrub it under the carpet where it still lives the same life(cycle -pun intended) as before.

Oke, so what is so bad about it living?

  1. It causes memory leaks.
  2. Your application still runs those queries AND wastes cycles and once it has finished doing so it does not use results.

Now that you are familiar with what is the result of that "solution" you have to choose. If this did not alarm you, fair enough, please at the minimum remove that ridiculous warning hiding code and at least keep the codebase clean.

If you got all riled up how you overlooked it and are willing to find a better solution, let's go:

CASE 1. If the async part of the app causing this is actually API queries, you should cancel your requests.If you are not using you should seriously consider using FetchAPI and then you can use AbortControllerAPI. Yes it is that simple and there are numerous other advantages over XMLHttpRequest.

In code (using FetchAPI) it looks like this:

const controller = new AbortController();
const { signal } = controller;

fetch("http://localhost:8000", { signal }).then(response => {
    console.log(`Request 1 is complete!`);
}).catch(e => {
    console.warn(`Fetch 1 error: ${e.message}`);
});

fetch("http://localhost:8000", { signal }).then(response => {
    console.log(`Request 2 is complete!`);
}).catch(e => {
    console.warn(`Fetch 2 error: ${e.message}`);
});

// Wait 2 seconds to abort both requests
// Hopefully you have not set your timeout lower than that somehow or it will fire before this
// catch will still fire off but the caught error would be different
setTimeout(() => controller.abort(), 2000);

Notice - when you cancel the request - the catch handlers fire off instead the actual response handlers that would process the data, set state etc. (don't notice I stole the code from David Walsh cause it is simple, well known solution, and most importantly - I am lazy)

NOTE: I get what you are saying but I use Axios / legacy code uses Axios and it uses XMLHttpRequest. True, it does, but it enhances it with the cancel mechanism. Just google axios.CancelToken and find out. Or, use Ky. It is IMHO basically better axios, based on FetchAPI which is superior API.

CASE 2. The async code causing this problem is NOT API queries therefore there is no built in cancel mechanisms. Well, WRITE ONE :) You are a programmer, aren't you?

NOTE: Deadline is coming, this is the last thing I needed, additional thinking and code, there is no time to think, I need sleep not additional work. I am just gonna hide it.

There, there, it will be oke - try:

  1. Check if the lib you are using already has its own (like axios or fileReaderAPI).
  2. The before mentioned AbortControllerAPI works in most browsers and NodeJS.
  3. Ignore the warning, don't be a prick and mask it :)
DanteTheSmith
  • 2,937
  • 1
  • 16
  • 31
1

Based on @ford04 answer, here is the same encapsulated in a method :

import React, { FC, useState, useEffect, DependencyList } from 'react';

export function useEffectAsync( effectAsyncFun : ( isMounted: () => boolean ) => unknown, deps?: DependencyList ) {
    useEffect( () => {
        let isMounted = true;
        const _unused = effectAsyncFun( () => isMounted );
        return () => { isMounted = false; };
    }, deps );
} 

Usage:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;
Nicolas
  • 31
  • 1
1

Depending on how you open your webpage, you may not be causing a mounting. Such as using a <Link/> back to a page that was already mounted in the virtual DOM, so requiring data from a componentDidMount lifecycle is caught.

  • Are you saying that `componentDidMount()` could be called twice without an intermediate `componentWillUnmount()` call in between? I don't think that's possible. – Alexis Wilke Dec 24 '20 at 18:35
  • 1
    No, I'm saying that it is not called twice which is why the page doesn't process the code inside ```componentDidMount()``` when using the ``````. I use Redux for these problems and keep the webpage's data in the Reducer store so that I don't need to reload the page anyways. – coder9833idls Jan 04 '21 at 10:13
1

Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then they react cannot update the state. here is the example code for this. write this line before every state Update.

if(!isScreenMounted.current) return;

Here is the Complete Code

import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
    const isScreenMounted = useRef(true)
    useEffect(() => {
        return () =>  isScreenMounted.current = false
    },[])

    const ConvertFileSubmit = () => {
        if(!isScreenMounted.current) return;
         setUpLoading(true)
 
         var formdata = new FormData();
         var file = {
             uri: `file://${route.params.selectedfiles[0].uri}`,
             type:`${route.params.selectedfiles[0].minetype}`,
             name:`${route.params.selectedfiles[0].displayname}`,
         };
         
         formdata.append("file",file);
         
         fetch(`${BASEURL}/UploadFile`, {
             method: 'POST',
             body: formdata,
             redirect: 'manual'
         }).then(response => response.json())
         .then(result => {
             if(!isScreenMounted.current) return;
             setUpLoading(false)    
         }).catch(error => {
             console.log('error', error)
         });
     }

    return(
    <>
        <StatusBar barStyle="dark-content" />
        <SafeAreaView>
            <ScrollView
            contentInsetAdjustmentBehavior="automatic"
            style={styles.scrollView}>
               <Text>Search Screen</Text>
            </ScrollView>
        </SafeAreaView>
    </>
    )
}

export default SearchScreen;


const styles = StyleSheet.create({
    scrollView: {
        backgroundColor:"red",
    },
    container:{
        flex:1,
        justifyContent:"center",
        alignItems:"center"
    }
})
Engr.Aftab Ufaq
  • 3,356
  • 3
  • 21
  • 47
1

I solved this problem by providing all the params that are used in the useEffect hook

The code reported the bug:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId]);

The code after fix:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId,level]);

Can see that , problems solved after I provided all the params(including the level param) that supposed to pass through.

1

2023

This is typically happening when running setState after waiting for an asynchronous function. If the component is no longer mounted when the response arrives, attempting to set state when the response arrives will result in the error message you are seeing.

useEffect(
  () => {
    someAsyncFunction().then(data => {
      setData(data)  // ❌ unsafe call of setData
    })
  },
  …
)

We can fix this by setting a local flag in the effect, and using the effect clean-up function to switch the flag when the component unmounts. The Fetching Data guide from the React docs has more details -

useEffect(
  () => {
    let mounted = true // ✅ component is mounted
    someAsyncFunction().then(data => {
      if (mounted) setData(data)  // ✅ setState only if mounted
    })
    return () => {
      mounted = false // ✅ component is unmounted
    }
  },
  …
)

custom hook, no callbacks

But can you imagine writing all of that each time you needed to run an asynchronous function in one of your components? Thankfully we can easily encapsulate this logic in a custom hook.

Other answers here provide a clumsy API that ask the user to specify onSuccess or onError callbacks. This reminds me of the code everyone was writing before we had promises. Let's see if we can do better -

import { DependencyList, useEffect, useState } from "react"

type UseAsyncHookState<T> =
  | { kind: "loading" }
  | { kind: "error", error: Error }
  | { kind: "result", data: T }

function useAsync<T>(func:() => Promise<T>, deps: DependencyList) {
  const [state, setState] = useState<UseAsyncHookState<T>>({ kind: "loading" })
  useEffect(
    () => {
      let mounted = true
      func()
        .then(data => mounted && setState({ kind: "result", data }))
        .catch(error => mounted && setState({ kind: "error", error }))
      return () => {
        mounted = false
      }
    },
    deps,
  )
  return state
}

Our hook helps us detangle complex API logic from component state and lifecycle. With a well-defined type and simple API method -

type User = { … }

async function getUser(id: number): Promise<User> {
  return …
}

We can write our component in declarative way, without the need for callbacks -

function UserProfile(props: { userId: number }) {
  const user = useAsync( // ✅ type automatically inferred
    () => getUser(props.userId),  // async call
    [props.userId],               // dependencies
  )
  switch (user.kind) { // ✅ exhaustive switch..case
    case "loading":
      return <Loading />
    case "result":
      return <UserData user={user.data} />
    case "error":
      return <Error message={user.error.message} />
  }
}

This also takes advantage of TypeScript 5's new switch..case exhaustive completions, ensuring that our component correctly checks for all state possibilities of the hook.

vanilla javascript

For non-TypeScript users, here's the vanilla hook that does the exact same thing -

import { useState, useEffect } from "react"

function useAsync(func, deps) {
  const [state, setState] = useState({ kind: "loading" })
  useEffect(
    () => {
      let mounted = true
      func()
        .then(data => mounted && setState({ kind: "result", data }))
        .catch(error => mounted && setState({ kind: "error", error }))
      return () => {
        mounted = false;
      }
    },
    deps
  )
  return state
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
0

I had a similar problem and solved it :

I was automatically making the user logged-in by dispatching an action on redux ( placing authentication token on redux state )

and then I was trying to show a message with this.setState({succ_message: "...") in my component.

Component was looking empty with the same error on console : "unmounted component".."memory leak" etc.

After I read Walter's answer up in this thread

I've noticed that in the Routing table of my application , my component's route wasn't valid if user is logged-in :

{!this.props.user.token &&
        <div>
            <Route path="/register/:type" exact component={MyComp} />                                             
        </div>
}

I made the Route visible whether the token exists or not.

Bulent Balci
  • 644
  • 5
  • 11
0

In my case the issue was that the parent component was hidding the child because of a condition change in the child component.

So what I did was to change the condition so the child component was always shown.

What was happening:

const ParentComponent:FC = () => {
  ...
  if (someCondition) {
    return null;
  }

  return (
   <>
     Some cool text here
    
     <ChildModalComponent message="this is a cool modal" />
   </>
  )
}



const ChildModalComponent: FC = () => {
  ...
  const handleSubmit = () => {
    setSomeCondition(true);
  }
}

So after clicking submit the modal was automatically hidden becasue of the parent condition (someCondition).

How did I fix it?

I changed the place where the someCondition was checked in the Parent component, so the child component was always shown:

const ParentComponent:FC = () => {
  ...
  return (
   <>
     {!someCondition && <>Some cool text here</>
    
     <ChildModalComponent message="this is a cool modal" />
   </>
  )
}
dani24
  • 2,108
  • 2
  • 26
  • 28
0

I faced same warning, not it is fixed. To fix the issue, I removed the useRef() variable check in useEffect() Earlier, the code was

const varRef = useRef();
useEffect(() => {
    if (!varRef.current)
    {
    }
}, []);

Now, the code is

const varRef = useRef();   
useEffect(() => {
    //if (!varRef.current)
    {
    }
}, [])

Hope, it helps...

Raj
  • 1
0

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

this error message is very clear. one example would be this

try {
  setIsLoading(true);

  ... you run some logic
  unmountingCode
  setError(null);
} catch (error) {
  setError(error.message);
}

this kind of try/catch logic is very common for UI control. we run some logic, then we unmoun the current component and then we are setting state with setError(null). this will create the above error message. to fix this, set the state first and then unmount

setError(null);
unmountingCode
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
0

For me I got this issue while my app open another page meanwhile it gets all the data after login; I mean in my function login I set states of account before I get company data causing my app switcher to load CreateComapnyApp component before the process was accomplished

Here is how my login function was before

  const login = React.useCallback(
    async ({ username, password }) => {
      ...
      const accountData = await getAccountData(token)
      setAccount(accountData)
      let companyData = null
      if (accountData.is_verified) {
        companyData = await getCompanyByUser(accountData.id)
      }
      setCompany(companyData)
      return accountData
    },
    [getAccountData, setAccount, setCompany, setToken],
  )

These is the app switcher

function AppSwitcher() {
  const { account, company } = useAuth()

  if (account?.is_verified && !company) {
    return <CreateCompanyApp />
  }

  if (account?.is_verified && company) {
    return <AuthenticatedApp />
  }

  return <UnAuthenticatedApp />
}

Explanation setAccount(accountData) causes the app to re-render so the condition if (account?.is_verified && !company) is verified the component CreateCompanyApp is rendered after that login continue processing and get company data now if (account?.is_verified && company) is verified, this setAccount before getting company data what caused Can't perform a React state update on an unmounted component issue

The solution is simply to move setAccount after getting company data so the condition if (account?.is_verified && company) is verified directly

  const login = React.useCallback(
    async ({ username, password }) => {
      ...
      const accountData = await getAccountData(token)
      let companyData = null
      if (accountData.is_verified) {
        companyData = await getCompanyByUser(accountData.id)
      }
      setAccount(accountData) //here we go
      setCompany(companyData)
      return accountData
    },
    [getAccountData, setAccount, setCompany, setToken],
  )
DINA TAKLIT
  • 7,074
  • 10
  • 69
  • 74
-1

Inspired by the accepted answer by @ford04 I had even better approach dealing with it, instead of using useEffect inside useAsync create a new function that returns a callback for componentWillUnmount :

function asyncRequest(asyncRequest, onSuccess, onError, onComplete) {
  let isMounted=true
  asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete)
  return () => {isMounted=false}
}

...

useEffect(()=>{
        return asyncRequest(()=>someAsyncTask(arg), response=> {
            setSomeState(response)
        },onError, onComplete)
    },[])

guneetgstar
  • 621
  • 7
  • 15
-1
const handleClick = async (item: NavheadersType, index: number) => {
    const newNavHeaders = [...navheaders];
    if (item.url) {
      await router.push(item.url);   =>>>> line causing error (causing route to happen)
      // router.push(item.url);  =>>> coreect line
      newNavHeaders.forEach((item) => (item.active = false));
      newNavHeaders[index].active = true;
      setnavheaders([...newNavHeaders]);
    }
  };
-1

The simplest and most compact solution (with an explanation) is seen below as a one-liner solution.

useEffect(() => { return () => {}; }, []);

The useEffect() example above returns a callback function triggers React to finish its unmount portion of its life-cycle prior to updating state.

That very simplistic solution is all that is needed. In addition, it also works unlike the fictional syntax provided by @ford04 and @sfletche . By the way, the below code snippet from @ford04 is purely imaginary syntax (@sfletche , @vinod , @guneetgstar , and @Drew Cordano used the very same imaginary syntax).

data => {       <--- Fictional/Imaginary Syntax

someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })

All of my linters and all the linters of my entire team will not accept it and they report Uncaught SyntaxError: unexpected token: '=>'. I am surprised that no one caught the imaginary syntax. Would anyone who has participated in this question-thread, particularly among the up-voters, explain to me how they got the solutions to work for them?

Devyn Collier Johnson
  • 4,124
  • 3
  • 18
  • 39
  • Your claim about "imaginary syntax" is a mistake. It works for many other folks! If your `someAsynchronousOperation()` return value type is `Promise` then sure `data` will cause a TypeScript compilation error. However, if it's `Promise` where `X` is not `undefined`/`void`/`never`, you'd definitely be able to use `.then(data => {...})`! You're have not provided a complete minimal example to reason about it. If you want your specific code issue resolved, open a separate question on StackOverflow. You don't want to get downvotes or have the answer flagged. – Igor Soloydenko May 27 '21 at 07:38
  • 1
    Are you talking about the arrow function? This was introduced in ES6. I don't know how you've configured your linter but this is very common syntax. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions – James Xabregas Jun 19 '21 at 11:37
  • Well I didn't down vote you... Without knowing anything about your linter settings, or anything about your tech stack I can't say for sure why that appears as a syntax error for you, but perhaps it's because your linter is using settings for older versions of Javascript that don't support this, but that's just a guess. – James Xabregas Nov 29 '21 at 21:43
  • I am surprised that no one caught the imaginary syntax. Use Babel, update linter settings, thank me later :) ES6 is so old. – DanteTheSmith Mar 24 '23 at 10:37
-2

Inspired by @ford04 answer I use this hook, which also takes callbacks for success, errors, finally and an abortFn:

export const useAsync = (
        asyncFn, 
        onSuccess = false, 
        onError = false, 
        onFinally = false, 
        abortFn = false
    ) => {

    useEffect(() => {
        let isMounted = true;
        const run = async () => {
            try{
                let data = await asyncFn()
                if (isMounted && onSuccess) onSuccess(data)
            } catch(error) {
                if (isMounted && onError) onSuccess(error)
            } finally {
                if (isMounted && onFinally) onFinally()
            }
        }
        run()
        return () => {
            if(abortFn) abortFn()
            isMounted = false
        };
    }, [asyncFn, onSuccess])
}

If the asyncFn is doing some kind of fetch from back-end it often makes sense to abort it when the component is unmounted (not always though, sometimes if ie. you're loading some data into a store you might as well just want to finish it even if component is unmounted)

Morten Moe
  • 11
  • 1