Async Vue lifecycles

Viewed 271

I have the following situation in my Vue.js application:

data() {
   return {
      data: []
   }
},

async created() {
   console.log('before async call')
   try {
      // async call ...
      console.log('after async call')
      this.data = // data from api call
   } catch (err) {
      // handle err
   }
},

mounted() {
   console.log('mounted')
   init(this.data)
}

When I run the code I get:

before async call
mounted
after async call

As a result of this the init method which is a class constructor in mounted gets called with an empty array instead of the data from the API call. What I would like is execute things synchronously and not execute mounted until the data is available. I understand that the above problem is how Vue executes lifecycles when there is async code included, but how can you solve problems like these?

2 Answers

async lifeCycles in Vue are a misleading syntax.

Every Vue lifecycle is only a trigger to run whatever code you put in there at that particular time.

But Vue doesn't wait for the promise to resolve and hold everything else (related to the component's lifecycle) until it happens. In effect, all you do is delay the execution of the code you place in the lifecycle until some promise resolves.

To give you a better understanding of what's going on, the following syntaxes are equivalent:

async created() {
  const data = await fetchSomeData(); 
  // rest of code depending on `data`
}

The equivalent:

created() {
  fetchSomeData().then(data => {
    // rest of code depending on `data`
  });
}

Because async lifecycles are a misleading syntax, it's typically discouraged in applications developed by large teams, in favor of the .then() syntax. This is to avoid minor bugs created by misunderstanding when code actually runs. For example, if a new developer places some code into an async hook (without looking closely at the rest of the code in the hook), the code might run later than intended, unless placed before any await.


To fix whatever errors you're trying to fix, just wrap what can't be rendered until actual data has resolved into a if (inside component) or v-if (inside template).

Typical usage examples:

computed: {
  someComputed() {
    if (this.data.length) {
      // returned when data has length
      return something
    }
    // returned when data has no length
    return somethingElse
  }
}

or:

<div v-if="data.length">
   <!-- markup depending on actual data... -->
</div>
<div v-else>
   loading...
</div>

Note: the computed above will automatically react to the change in data's length (no need for a watch), because computed properties get re-run whenever their internal reactive references change values. As you'd expect, they recalculate/rerender anything depending on them. Same is true for <v-if>. Internally they both use what's known as Vue's "injections and reactivity".

If I were you, I would put all the logic within either mounted or created hook. If for some reason you need to wait until mount for your code to work, fetch the data and init it within mounted:

async mounted() {
   try {
      // async call ...
      console.log('after async call')
      this.data = await // data from api call
      init(this.data)
   } catch (err) {
      // handle err
   }
}

You've almost nothing to gain in terms of performance to split the code between created and mounted.

Still, if for some reason you really need to place them in different hooks, you could store data in a promise:

async created() {
   console.log('before async call')
   try {
      // async call ...
      console.log('after async call')
      this.data = fetch('whatever') // now this.data is a promise
   } catch (err) {
      // handle err
   }
},

async mounted() {
   console.log('mounted')
   init(await this.data)
}

This should work, but I don't think it would be worth the hassle.

As a clarification for whoever may read this: you can use async/await in vue lifecycle hooks in order to code within to await for promises etc. However, this won't make Vue to actually await for the lifecycles itselves: so you can end withmounted code running before async code from created. The fact that Vue doesn't wait for async lifecycles doesn't mean is bad to place async/await code in them.

Related