202

Is using componentDidMount() as an async function good practice in React Native or should I avoid it?

I need to get some info from AsyncStorage when the component mounts, but the only way I know to make that possible is to make the componentDidMount() function async.

async componentDidMount() {
    let auth = await this.getAuth();
    if (auth) 
        this.checkAuth(auth);
}

Is there any problem with that and are there any other solutions to this problem?

Sid Mhatre
  • 3,272
  • 1
  • 19
  • 38
Mirakurun
  • 4,859
  • 5
  • 16
  • 32

9 Answers9

204

Let's start by pointing out the differences and determining how it could cause troubles.

Here is the code of async and "sync" componentDidMount() life-cycle method:

// This is typescript code
componentDidMount(): void { /* do something */ }

async componentDidMount(): Promise<void> {
    /* do something */
    /* You can use "await" here */
}

By looking at the code, I can point out the following differences:

  1. The async keywords: In typescript, this is merely a code marker. It does 2 things:
    • Force the return type to be Promise<void> instead of void. If you explicitly specify the return type to be non-promise (ex: void), typescript will spit an error at you.
    • Allow you to use await keywords inside the method.
  2. The return type is changed from void to Promise<void>
    • It means you can now do this:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. You can now use await keyword inside the method and temporarily pause its execution. Like this:

    async componentDidMount(): Promise<void> {
        const users = await axios.get<string>("http://localhost:9001/users");
        const questions = await axios.get<string>("http://localhost:9001/questions");
    
        // Sleep for 10 seconds
        await new Promise(resolve => { setTimeout(resolve, 10000); });
    
        // This line of code will be executed after 10+ seconds
        this.setState({users, questions});
        return Promise.resolve();
    }
    

Now, how could they cause troubles?

  1. The async keyword is absolutely harmless.
  2. I cannot imagine any situation in which you need to make a call to the componentDidMount() method so the return type Promise<void> is harmless too.

    Calling to a method having return type of Promise<void> without await keyword will make no difference from calling one having return type of void.

  3. Since there is no life-cycle methods after componentDidMount() delaying its execution seems pretty safe. But there is a gotcha.

    Let's say, the above this.setState({users, questions}); would be executed after 10 seconds. In the middle of the delaying time, another ...

    this.setState({users: newerUsers, questions: newerQuestions});

    ... were successfully executed and the DOM were updated. The result were visible to users. The clock continued ticking and 10 seconds elapsed. The delayed this.setState(...) would then execute and the DOM would be updated again, that time with old users and old questions. The result would also be visible to users.

=> It is pretty safe (I'm not sure about 100%) to use async with componentDidMount() method. I'm a big fan of it and so far I haven't encountered any issues which give me too much headache.

Cù Đức Hiếu
  • 5,569
  • 4
  • 27
  • 35
  • When you talk about the issue where another setState occurred before a pending Promise, isn't that the same with Promise without the async/await syntactic sugar or even classic callbacks ? – Clafouti Feb 25 '18 at 12:18
  • 3
    Yes! Delaying a `setState()` always possesses a small risk. We should proceed with care. – Cù Đức Hiếu Feb 25 '18 at 13:31
  • I guess one way to avoid problems is to use something like `isFetching: true` inside a component's state. I've only used this with redux but I suppose it's completely valid with react-only state management. Although It doesn't really solve the problem of the same state being updated somewhere else in the code... – Clafouti Feb 25 '18 at 14:03
  • 1
    I agree with that. In fact, the `isFetching` flag solution is pretty common especially when we want to play some animations in front-end while waiting for back-end response (`isFetching: true`). – Cù Đức Hiếu Feb 25 '18 at 14:25
  • 4
    You can run into problems if you do setState after the component is unmounted – Eliezer Steinbock May 02 '18 at 06:31
  • How about not changing the signature of a methods you do not own. It will be safer to use promise callback to set the state. – Morlo Mbakop May 20 '19 at 09:36
  • You say:"Calling to a method having return type of Promise without await keyword will make no difference from calling one having return type of void". Are you sure? For me it looks dramatically different. If you call the async function without await, the execution continues immediately, regardless whether the function completes or not. Most importantly, you will not see any error from the async function, they will silently put into the returned promise which you ignore. So the question actually is: Does React process the returned promise correctly? Does it handle errors from the Promise? – C-F Jun 28 '19 at 02:13
  • @C-F: React just calls the `ComponentDidMount()` method and doesn't care about its return type. Here is the link to the relevant part of the React source code (line 433): https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.js#L433 – Cù Đức Hiếu Jun 29 '19 at 08:33
  • @CùĐứcHiếu Thank you very much for the source code link. Now I know for sure what is happening. I have already done some experimenting and found out the same: React does not care about the promise. I have posted my answer below https://stackoverflow.com/a/56800470/986720 – C-F Jun 30 '19 at 22:59
36

Update April 2020: The issue seems to be fixed in latest React 16.13.1, see this sandbox example. Thanks to @abernier for pointing this out.


I have made some research, and I have found one important difference: React does not process errors from async lifecycle methods.

So, if you write something like this:

componentDidMount()
{
    throw new Error('I crashed!');
}

then your error will be caught by the error boundary, and you can process it and display a graceful message.

If we change the code like this:

async componentDidMount()
{
    throw new Error('I crashed!');
}

which is equivalent to this:

componentDidMount()
{
    return Promise.reject(new Error('I crashed!'));
}

then your error will be silently swallowed. Shame on you, React...

So, how do we process errors than? The only way seems to be explicit catch like this:

async componentDidMount()
{
    try
    {
         await myAsyncFunction();
    }
    catch(error)
    {
        //...
    }
}

or like this:

componentDidMount()
{
    myAsyncFunction()
    .catch(()=>
    {
        //...
    });
}

If we still want our error to reach the error boundary, I can think about the following trick:

  1. Catch the error, make the error handler change the component state
  2. If the state indicates an error, throw it from the render method

Example:

class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }
  
  buggyAsyncfunction() { return Promise.reject(new Error('I crashed async!'));}
  
  async componentDidMount() {
    try
    {
      await this.buggyAsyncfunction();
    }
    catch(error)
    {
        this.setState({error: error});
    }
  }
  
  render() {
    if(this.state.error)
        throw this.state.error;
        
    return <h1>I am OK</h1>;
  }
}
kelsny
  • 23,009
  • 3
  • 19
  • 48
C-F
  • 1,597
  • 16
  • 25
  • is there an issue reported for this ? Could be useful to report it if still the case... thx – abernier Apr 26 '20 at 16:36
  • @abernier I think it is by deign... Though probably they could improve it. I did not file any issues about this... – C-F Apr 27 '20 at 10:38
  • 1
    it seems not to be the case anymore, at least with React 16.13.1 as tested here: https://codesandbox.io/s/bold-ellis-n1cid?file=/src/App.js – abernier Apr 27 '20 at 13:34
  • 1
    We are using React Native 0.63.4 with React 16.13.1, and we still have the issue of unhandled promise rejections not being caught by the error boundary. – Jarod Legault Jun 25 '21 at 16:08
  • 3
    Actually, @abernier, if you dismiss the error window in the codesandbox, you will see that the page still renders. I [forked your codesandbox](https://codesandbox.io/s/staging-sun-rf2bk?file=/src/App.js) and added an error boundary. If you remove the `async` from `componentDidMount`, you will see the error boundary catch the error. If you leave it in, no error is caught. Just make sure you dismiss the error message window. – Jarod Legault Jun 25 '21 at 17:16
11

Your code is fine and very readable to me. See this Dale Jefferson's article where he shows an async componentDidMount example and looks really good as well.

But some people would say that a person reading the code may assume that React does something with the returned promise.

So the interpretation of this code and if it is a good practice or not is very personal.

If you want another solution, you could use promises. For example:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}
Tiago Alves
  • 2,290
  • 13
  • 32
  • 3
    ...or also, just use an inline `async` function with `await`s inside...? – Erik Kaplun Mar 12 '18 at 11:26
  • also an option @ErikAllik :) – Tiago Alves Mar 15 '18 at 13:24
  • @ErikAllik do you happen to have an example ? – Pablo Rincon Apr 25 '18 at 02:24
  • 1
    @PabloRincon smth like `(async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()` where `fetch` and `submitRequest` are functions that return promises. – Erik Kaplun May 02 '18 at 14:07
  • 1
    This code is definitely bad, because it will swallow any error occurred in getAuth function. And if the function does something with the network (for example), errors must be expected. – C-F Jun 28 '19 at 02:00
8

When you use componentDidMount without async keyword, the doc say this:

You may call setState() immediately in componentDidMount(). It will trigger an extra rendering, but it will happen before the browser updates the screen.

If you use async componentDidMount you will loose this ability: another render will happen AFTER the browser update the screen. But imo, if you are thinking about using async, such as fetching data, you can not avoid the browser will update the screen twice. In another world, it is not possible to PAUSE componentDidMount before browser update the screen

Lu Tran
  • 401
  • 4
  • 9
  • 1
    I like this answer because it's concise and supported by docs. Can you please add a link to the docs you are referencing. – theUtherSide May 31 '19 at 16:31
  • This might even be a good thing e.g. if you are displaying a loading state while the resource is loading and then the contents when it is done. – Hjulle Mar 26 '20 at 12:29
4

I think it's fine as long as you know what you're doing. But it can be confusing because async componentDidMount() can still be running after componentWillUnmount has run and the component has unmounted.

You may also want to start both synchronous and asynchronous tasks inside componentDidMount. If componentDidMount was async, you would have to put all the synchronous code before the first await. It might not be obvious to someone that the code before the first await runs synchronously. In this case, I would probably keep componentDidMount synchronous but have it call sync and async methods.

Whether you choose async componentDidMount() vs sync componentDidMount() calling async methods, you have to make sure you clean up any listeners or async methods that may still be running when the component unmounts.

dosentmatter
  • 1,494
  • 1
  • 16
  • 23
3

Update:

(My build: React 16, Webpack 4, Babel 7):

When using Babel 7 you'll discover:

Using this pattern...

async componentDidMount() {
    try {
        const res = await fetch(config.discover.url);
        const data = await res.json();
        console.log(data);
    } catch(e) {
        console.error(e);
    }
}

you will run into the following error...

Uncaught ReferenceError: regeneratorRuntime is not defined

In this case you will need to install babel-plugin-transform-runtime

https://babeljs.io/docs/en/babel-plugin-transform-runtime.html

If for some reason you do not wish to install the above package (babel-plugin-transform-runtime) then you will want to stick to the Promise pattern...

componentDidMount() {
    fetch(config.discover.url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })
    .catch(err => console.error(err));
}
Chad
  • 579
  • 4
  • 9
2

I like to use something like this

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
1

Actually, async loading in ComponentDidMount is a recommended design pattern as React moves away from legacy lifecycle methods (componentWillMount, componentWillReceiveProps, componentWillUpdate) and on to Async Rendering.

This blog post is very helpful in explaining why this is safe and providing examples for async loading in ComponentDidMount:

https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html

DannyMoshe
  • 6,023
  • 4
  • 31
  • 53
  • 5
    Async rendering actually has nothing to do with making lifecycle explicitly async. It's actually an anti-pattern. The recommended solution is to actually call an async method from a lifecycle method – Clayton Ray Sep 23 '19 at 15:29
1

To Tag on to @C-F's answer, I added a typescript decorateor (AsyncMethodErrorHandler) to handle errors in async componentDidMount() and other async methods that fail to bubble up errors to the application state.

I found this easier than wrapping dozens of async methods in a try/catch block in an app whose maintainince I inherited.

class BuggyComponent extends React.Component<{error_message?:string}> {

  @AsyncMethodErrorHandler("error_message")
  async componentDidMount() {
      await things_that_might_fail();
  }

  render(){
      if(this.state.error_message){
          return <p>Something went wrong: {this.state.error_message}</p>
      }
  }
}


function AsyncMethodErrorHandler(
  /* Key in the this.state to store error messages*/
  key: string,
  /* function for transforming the error into the value stored in this.state[key] */
  error_handler: string | { (e: Error): string } = (e: Error) => e.message
) {

  return function (
    cls: React.Component,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const f: { (...args: any[]): Promise<any> } = descriptor.value;
    return {
      ...descriptor,
      value: function (...args: any[]) {
        return f.apply(this, args).catch((e: Error) => {
          console.log(`an error occured in the ${propertyKey} Method:`, e);
          (this as any as React.Component).setState({
            [key]:
              typeof error_handler === "string"
                ? error_handler
                : error_handler(e),
          });
        });
      },
    };
  };
}

Note that as of this writing, this solution does not work for async function properites because:

property decorator[s] can only be used to observe that a property of a specific name has been declared for a class

Jthorpe
  • 9,756
  • 2
  • 49
  • 64