Vue: Await async component lifecycle hooks

Created on 8 Dec 2017  路  51Comments  路  Source: vuejs/vue

What problem does this feature solve?

If a user needs to implement a lifecycle hook that depends on async operations, vue should respect the async nature of the implemented hook and await it in vue land.

What does the proposed API look like?

The API does not change; only how it works now by awaiting async hooks.

Most helpful comment

This is the actual code that I want to be awaited:

  beforeMount: async function() {
       this.user = await client.get({type: 'user', id: this.$route.params.id});
    }

Which would be part of the UserPage component.

All 51 comments

So you want

created () {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('created')
            resolve()
        })
    })
},
mounted () {
    console.log('mounted')
}

to display

mounted

?

When creating a feature request, please add a real-world use case to make the request worth being implemented.

While this is theoretically a cool idea, this requires a fundamental rethink/rewrite of the architecture to achieve, and can potentially break a lot of logic that relies on the synchronous nature of lifecycle hooks. So the benefits must be substantial in order to justify that change - otherwise this can only be considered when we plan to do a full breaking upgrade, which is unlikely to happen very soon.

Closing for now, but feel free to follow up with more concrete reasoning / use cases / implications.

@posva Understood -- I apologize. My actual use case is one where I have a UserPage component that receives a user_id from the page route parameters (through this.$route.params), then fetches the actual user data from the database on the server using a command like:
this.user = await client.get({type: 'user', id: this.$route.params.id})
where this.user refers to a user field in the data part of the UserPage component.

Ideally, I want that line of code to be executed after the component has been created (so that this.$route.params is available) but before the component is actually mounted so that in my template I can safely use user interpolations without getting any errors about undefined values.

@yyx990803
I might be a noob here, but shouldn't the only change be adding the await keyword before the call for lifecycle hooks like mounted, etc, in Vue land?

This is the actual code that I want to be awaited:

  beforeMount: async function() {
       this.user = await client.get({type: 'user', id: this.$route.params.id});
    }

Which would be part of the UserPage component.

No worries! I was imagining that use case. It's better to handle it as described in vue-router docs as it opens different ways of displaying loading state. You can already wait for the data to be there before rendering the component btw.

OK that makes sense. Now, however, what if I have a user component that is a stripped down version of the user page (say, like the component that appears when you hover over a user's name on facebook and "peek" into their profile), then the router is not involved here and the id will be passed as a property to the component.

Taking the big picture view here, functions in JavaScript can now be either synchronous or asynchronous, and that lifecycle hooks being functions, and the way we think of them as functions, should support asynchronousity (as demonstrated by my use case and intuitive "reach" for the approach I am using here).

You have many ways of doing it. The simplest one is using a variable that starts as null, fetch the data and set it, toggling the actual component (because of a v-if). A more exotic version would be a function that resolves the component and use a <component :is="myDynamicComp"/> 馃槃
But please, don't hijack the issue into a question 馃槈 use the forum or discord for that

No I really don't want help with the code! Actually I already have implemented workarounds very similar to your suggestions. What I am trying to say is that it is much more intuitive to just use JS async features.

The part I didn't realize is that asynchronous and synchronous code are fundamentally different in nature, so that synchronous code cannot be forced to adhere to asynchronous code without fundamentally changing itself to asynchronous code. yyx990803 saw it immediately but it took me some time to understand his comment completely. Thanks for your time guys and sorry if there was a miscommunication on my part somewhere through the way.

I have some use case here and would like to get some suggestion and workaround method.

MainPage.vue is my main container. I call ajax "/init" at beforeCreate to get user info and then commit to Vuex.store.
Content.vue is the child inside MainPage.vue. I would like to call different api calls at mounted stage according to user's role which come from Vuex.store.

If the lifecycle call in async/await flow, it would follow the order
Parent beforeCreate -> Parent create -> Child beforeCreate -> Child create -> Child mounted .... (If I understand correctly about component lifecycle).

But currently I cannot get user info at Content.vue, how can I get workaround now?
I would like to keep "/init" api called inside MainPage.vue because It is used in many pages(container in Vue-Router).

posted question on stackoverflow

Thanks

a hackish workaround for what is worth:

{
  created: function(){
    this.waitData = asyncCall();
  },
  mounted: function(){
    this.waitData.then(function(data) { ... })
  }
}


A possible more "flat" solution:

{
    async created () {
        let createdResolve = null
        let createdReject = null
        this.createdPromise = new Promise(function(resolve, reject){
            createdResolve = resolve
            createdReject = reject
        })
        await asyncCall1()
        await asyncCall2()
        ...
        createdResolve(someResult)
    }
    async mounted () {
        let result = await this.createdPromise
        ...
    }
    data () {
        return {
            createdPromise: null
        }
    }
}

Is this not a thing yet?

data() {
 ...
},
async created() {
  const something = await exampleMethod();
  console.log(something);
}

Is working for me (as @fifman mentions).

@breadadams Yes, of course. The functions _inside_ the created method will be awaited - however the created or mounted function itself is not.

So the Vue instance will call created and instantly mounted before any of the long running processes in created are finished

Ah, my bad @darren-dev - different use case on my side, but I see the issue now 馃槄 thanks for clarifying.

@breadadams No problem at all - we're all here for our own cases, glad I could clarify!

Async lifecycle hook can be a good implementation in next major version

Seems to me that allowing async support for the lifecycle methods will encourage bad UX practices by default. How? Async functions are used for requests that cannot be completed immediately (e.g. long-running or network requests). Forcing Vue to delay creation or mounting or any of the other lifecycle methods to wait on your network request or long-running asynchronous process will impact the user in noticeable ways. Imagine a user coming to your site and then having to wait for 4 seconds with a blank screen while the component waits for the user's spotty cell connection to finish your network request. And not only does this negatively impact the user but you are also sacrificing your control over the situation - there's nothing you can do as a developer to make the users perceived load time quicker, or show determinate or indeterminate progress indicators. So by building this ability into Vue you aren't making the web a better place; you're enabling bad practices.

Much better to plan and design for the asynchronous case from the get-go: kick off your asynchronous process in created or mounted or wherever, and then make your component have a skeleton structure or at worst a spinner while you wait for your API to return the user's permissions. Much better UX and you don't sacrifice any control. And Vue doesn't have to add code to deal with asynchronous lifecycle functions, keeping the bundle smaller. Win win.

@seanfisher You raise a valid point. Architecturally speaking, designing around an asynchronous set of events should be handled by the developer - as that's the only way to portray the message correctly.

Disclaimer: The following is written with the idea of page generation in mind. There are definitely valid use-cases where my argument is invalid.


However, dictating the design patterns of a developer should not be left up to the framework you're using. My argument is that if you're not waiting for a phase to complete - then why have different phases? Why have a created, then mounted stage? If everything is basically happening at once, then completely ignoring the created stage is okay.

Literally, the only time I've ever (Since early Vue) hooked into created was when I had to inject something vue needed to rely on - it had nothing to do with the setup or layout of my page. However, I have had to wait for (short) async tasks to run that would be much better _before_ the page got rendered (Such as hooking into Firebase's authentication methods). Having that in create, then waiting for it to complete before mounted would reduce the need for hacky workarounds completley.

Remember, my argument is that Vue shouldn't tell me that Im developing wrong. It should just provide the desired functionality.

However, dictating the design patterns of a developer should not be left up to the framework you're using.

Um....Frameworks are built to specifically limit or guide or "frame" the developer into certain design patterns and practices. That's their main purpose. Any good framework will offer a smart API, which precisely offers a clear and obvious way to work with the framework and yet it will also be constraining.

Yes, it is paradoxical that a framework offers certain abilities, yet also constrains the developer at the same time to certain design practices. That is exactly where opinionation within Frameworks can either help or hurt it. It's hard to find the right balance and I think Vue or rather Evan and the Vue dev team have done and are doing a great job making these judgement calls.

Scott

I'll never argue that a well designed framework should be extended with the same design pattern, but my argument is that the framework can't dictate it. You're correct, but I'm saying that no matter how good the framework, the end developer should still be open to do whatever they wanted.

But you haven't touched on the _actual_ argument of making the created and mounted events asyncs - you just added your opinion (which isn't wrong) on my opinion, which generally leads to a huge derail.

I'll never argue that a well designed framework should be extended with the same design pattern, but my argument is that the framework can't dictate it.

Sorry, but this makes no sense to me. Please show me one framework that doesn't dictate how it should be extended.

I thought my saying "Evan and Co making good judgement calls" would show my opinion. But, to be more clear. The mounted and created lifecycle hooks don't need to work asynchronously or rather, the fact they work synchronously helps with reasoning about the application logic. Any "waiting" needs to be accounted for in the UI anyway and asynchronous code can still be ran in each hook. We can argue about now necessary the beforeMount and mount hooks are. But, there might be a thing or two you might need in mounted, for instance, which you can't yet access in created, like the compiled render function.

Scott

If Vue has an opinion one way or the other on async lifecycle hooks it shouldn't be a matter of speculation. There is no need to speculate when the standards, APIs, guides and best practices Vue adopts, provides or recommends are available for all to read.

In Evan's original reply, async lifecycle hooks are not in the standard API not because it is necessarily a bad idea, but because the benefits are not substantial enough to justify the cost of implementation.

For the opinion that it is a bad practice to make the user wait for an async created or other lifecycle hook without an indicator that something is happening, the argument can be made that Vue, having supported async hooks, now maybe provides something which could be called "phase templates" that would also solve the problem (and which could be easy to implement).

For the opinion that it is a bad practice to make the user wait for an async created or other lifecycle hook without an indicator that something is happening,

Is this really even a problem?

Scott

Here is the issue I am having -- and maybe someone can suggest how I SHOULD be doing this because this appears problematic.

Our Vue application (rather large) uses Vuex extensively. In quite a few of our Vue components in the create() lifecycle we set via store.dispatch() some items in the store (obviously). However, as it has come to my attention -- store.dispatch() ALWAYS returns a promise .. even if the underlying logic and function is NOT async. So I put in async created() { await store.dispatch('foo/action') } but as noted that actually fails ..

I am also using Typescript and it complains rather bitterly when I don't await / .then the store.dispatch() calls .. having "floating" promises..

So what's the best way to use Vuex store.dispatch() in a lifecycle when we can't async them?

Cheers!!

All other discussion about vue's specific opinions, and whether frameworks should impose opinions aside, it could be beneficial to document this behavior more clearly.

I'm looking at @fifman 's "more flat" solution above, and I'm not sure it solves the issue for me, which is ensuring I've loaded my XHR by the time mounted() returns. In that solution, both created() and mounted() are async, so Vue will call each of them and they'll return more or less immediately, with the async stuff going on in the background. In fact I'm not sure how that's better than just doing away with the createdPromise and just doing all the async work in either created() or mounted(), like:

async mounted() {
  let response = await fetch(my_url)
  let data = await response.text()
  my_data_member = data
}

In any case, my_data_member will be filled in "later" when the XHR completes, long after mounted() returns its Promise, right?

You can also try to use v-if on root component <div id="app"/> and trigger it when your data loading finished. I, for example ,use it for refresh token.

This is a cool feature to have and have lots of use cases. For my usecase I need to load specific API properties before the actual render of the component. However, i don't feel that async in life cycle hooks is an ideal solution.

Currently everyone is mentioning here a perfect scenario where the async functions runs smoothly. But lets say for eg if the users connection is slow or lagging and we wait for the component to mount after the response of an API, then the whole component life cycle is distributed. We won't be able to show loading information to the user. Or far worse, if the API times out or returns error, the application will hang without mounting.

Though this is a cool feature to have, I would suggest for it to be handled by application rather then the framework because the domain logic implementation has more insight on the underlying logic then the framework thus creating less side effects.

+1 for this feature.
I have a mixin used by various components, the mixin fetches data from a database in the mounted hook.
The components have to work with the data fetched by the mixin also in the mounted hook.
As it is now, i had to implement a callback to be called when the mixin has finished loading the data and ditch the mounted hook in the component.
It works but would be prettier if the mounted hook handled promises.

@cederron purely out of interest, why can't you download the data in the parent component and pass it in as a prop? use a v-if to show a loading indicator while the data loads and when the data shows up, you can show the component, and when it is created, it will have all of the data that it needs.

Avoids having the component represent two distinct unrelated states ('no data loaded, waiting' and 'data has been loaded, you can manipulate it')

If you need to reuse the logic in multiple places, might even move the download logic to Vuex, kinda makes sense as downloading data would go in a Vuex action rather than a component.

@ReinisV I think I simplified my case too much, the component creates new data from the data fetched by the mixin mounted hook, and component view is binded to this new data.
So, the mixin has to fetch data from the database > the component creates data from it > now the component is shown

AFAIK this wont work:

export const MyMixin = {
    data: function () {
        return {
            dbData: null
        }
    },
   mounted:  async function () {
      this.dbData = await asyncFetchDataFromDB()
   }
}


export const MyComponent = {
    mixins: [MyMixin],
    data: function () {
        return {
            newData: null
        }
    },
   mounted:  function () {
      this.newData = handleDBData(this.dbData)
   }
}

dbData will be null in component mount hook.

Currently I execute a callback when the mixin fetches the data but would be prettier to just make the mount function async.

Can not say much about vuex as I'm not using it

I really want to reiterate what @seanfisher mentioned here. Having vue wait on components which have been marked as async not only causes issues for the users, the pattern of using async/await to start data look up is present everywhere. It would force explicitly converting the code in these lifecycle hooks to unawaited promises to explicitly avoid blocking vue. In some cases it might be good, and if a feature would be introduced, I would have to suggest running two lifecycles at the same time, the current one which handles component hooks execution and one which awaits those executions for global callbacks.

But I really would be disappointed to literally rewrite every one of my lifecycle hooks to avoid blocking vue, async/await is much cleaner, so we don't use promises. Changing this in a non-backwards compatible way changes the pit of success (unawaited component lifecycle) for a pit of failure.

Much better to plan and design for the asynchronous case from the get-go: kick off your asynchronous process in created or mounted or wherever, and then make your component have a skeleton structure or at worst a spinner while you wait for your API to return the user's permissions. Much better UX and you don't sacrifice any control. And Vue doesn't have to add code to deal with asynchronous lifecycle functions, keeping the bundle smaller. Win win.

@seanfisher Thank you, this is super helpful!

Why not using an asynchronous component instead, which will be rendered only when asynchronous calls have returned?
more info on the API here
https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components

new Vue({
  components: {
    root: () => ({ // Aync component
      // The component to load (should be a Promise)
      component: new Promise(async function (resolve) {
        await FetchMyVariables()
        resolve(MyComponent) // MyComponent will be rendered only when FetchMyVariables has returned
      }),
      // A component to use while the async component is loading
      loading: { render: (h) => h('div', 'loading') }, 
    })
  },
  render: h => h('root')
})

while most of these solutions seem fine, I think this is one of those major missing pieces of Vue that make it so intuitive. I think Vue 3 needs to implement this as we have come to the point where using async await is now an everyday thing. SO PLEASE @yyx990803 You, let's have it in Vue 3. PLEEEEEEEASE. The whole VUE architecture was made without assumption to these cases and most of the things that people are posting here are just workarounds and hackish. I think hooks should actually respect async functions and also expect return values too that are then passed on to the next hook function.

I am going to refactor my code seeing this isn't being honoured, but ugly code will come out of it since it'd be a hack.

Seems to me that allowing async support for the lifecycle methods will encourage bad UX practices by default. How? Async functions are used for requests that cannot be completed immediately (e.g. long-running or network requests). Forcing Vue to delay creation or mounting or any of the other lifecycle methods to wait on your network request or long-running asynchronous process will impact the user in noticeable ways. Imagine a user coming to your site and then having to wait for 4 seconds with a blank screen while the component waits for the user's spotty cell connection to finish your network request. And not only does this negatively impact the user but you are also sacrificing your control over the situation - there's nothing you can do as a developer to make the users perceived load time quicker, or show determinate or indeterminate progress indicators. So by building this ability into Vue you aren't making the web a better place; you're enabling bad practices.

Much better to plan and design for the asynchronous case from the get-go: kick off your asynchronous process in created or mounted or wherever, and then make your component have a skeleton structure or at worst a spinner while you wait for your API to return the user's permissions. Much better UX and you don't sacrifice any control. And Vue doesn't have to add code to deal with asynchronous lifecycle functions, keeping the bundle smaller. Win win.

One opinion of thousands. Just because you can't imagine a scenario where component rendering being delayed can live alongside a positive user experience doesn't mean it doesn't exist.

If a framework fights the developer the developer will find another framework.

@robob4him

One opinion of thousands. Just because you can't imagine a scenario where component rendering being delayed can live alongside a positive user experience doesn't mean it doesn't exist.

If a framework fights the developer the developer will find another framework

You are absolutely right, but nothing you shared here is a valid argument to solve the problem one way or another. You've introduced a scare tactic to coerce the community into developing a feature. Not a constructive continuation of the conversation.

@wparad, it's absolutely a scare tactic, but it's not going to coerce anyone into doing anything. Putting forth arguments that a feature supports bad habits or anti-patterns or will decay the larger community of developers is just as much a scare tactic.

Half the features of literally any framework/language are hazardous to a developer; public methods can be extended, Vue encourages access to the element ($el), etc. Frameworks provide these things because at the end of the day the developer needs to get the job done.

This feature request is a year old. People should understand that the reason is not actually because it would cause bad practices nor should they perceive delayed rendering as a bad practice.

I need to use requirejs with vue. Not that I like requirejs but because I want to use vue with an open source LMS which has all it's modules as AMD modules. It'd be great if I could load all the libs I need in the beforeCreate hook. The alternative for me at the moment is to load them outside of vue and then pass them in which is messier.

Seems to me that allowing async support for the lifecycle methods will encourage bad UX practices by default. How? Async functions are used for requests that cannot be completed immediately (e.g. long-running or network requests). Forcing Vue to delay creation or mounting or any of the other lifecycle methods to wait on your network request or long-running asynchronous process will impact the user in noticeable ways. Imagine a user coming to your site and then having to wait for 4 seconds with a blank screen while the component waits for the user's spotty cell connection to finish your network request. And not only does this negatively impact the user but you are also sacrificing your control over the situation - there's nothing you can do as a developer to make the users perceived load time quicker, or show determinate or indeterminate progress indicators. So by building this ability into Vue you aren't making the web a better place; you're enabling bad practices.

Much better to plan and design for the asynchronous case from the get-go: kick off your asynchronous process in created or mounted or wherever, and then make your component have a skeleton structure or at worst a spinner while you wait for your API to return the user's permissions. Much better UX and you don't sacrifice any control. And Vue doesn't have to add code to deal with asynchronous lifecycle functions, keeping the bundle smaller. Win win.

What you are saying is like adding v-if/v-clock/v-show features will encourage bad practices so we better fool-proof the framework by removing those features. Then use some convoluted approach to do the same so that Vue is smaller for not putting those 3 directives. 1st developers aren't stupid. 2nd fool-proofing the framework in turn limits its usability because you are limiting what can be done based on apparent "fools". Why would one put a v-if for their entire website to block it thru async operations leaving the entire screen blank?

I think you are ignoring the fact that most of the use cases may not even be with a blank page. They are called components for a reason. Cases where I personally want to use this is where something is already on the screen doing something else. This maybe a component blocked by a v-if for instance and triggered when something changes. However, in the process of rendering it for the first time, async functions etc need to be respected as the component boots. I grazed the entire Vue documentation looking for this and finally did it with a very not so pretty workaround like above hackish examples.

What worries me is someone/even later me maintaining the code. It's like the Promise callback hell vs Async ... Await.

Actually I see it enhancing the framework's flexibility and controllability in an easy to followup fashion. Just take a look at the hacks above to see what I mean. Developers are doing all that just to fill in the gap of a simple async mounted () { await... } statement for example. If you don't want to use those features, you just don't define the functions as async or even use await at all.

Actually someone who will actually use an async mounted lifecycle hook will most likely understand what they are doing and why they will be doing it, and will most likely NOT make those bad practices you are worried about.

@emahuni, I don't think any would disagree with the expectation you are sharing, but I'm thinking there is a nuance, that is being left out. Let's assume that an async mounted or async created delays the component from being rendered. What does the parent do in this case? Does it:

  • block as well
  • assume that the component is meant to be removed the DOM v-if until it is done loading
  • assume that the component is meant to be hidden until completed loading
  • Show a temporary element in its place?

While I agree that the expectations around a dynamically loaded component are consistent, the behavior for the parent I don't think would be. In these cases, it would be IMO exposing the implementation of the child to the parent and force the parent to figure out what to do with that dynamic component. Instead how the data is loaded and the state of the child component should be up to the child. If it loads async then it needs some way to explain to Vue what should be rendered in its place (or not rendered). The best way to handle that is how the framework already works, rather than introducing a new complexity.

Further, I'm not totally following your argument though:

What you are saying is like adding v-if/v-clock/v-show features will encourage bad practices so we better fool-proof the framework by removing those features

While in this case, we can clearly see that introducing async components awaiting the mount or created will cause bad practices, it isn't clear that this does. Second, it is begging the question, even if those do cause bad practices, we should opt for fixing them, rather than using them as a justification for create new bad practices. If you know of bad practices that are being created by v-if, etc... I invite you share explicitly (in another issue of course) what the problem with those are, rather than trying to use that as a justification for a different discussion.

@emahuni, I don't think any would disagree with the expectation you are sharing, but I'm thinking there is a nuance, that is being left out. Let's assume that an async mounted or async created delays the component from being rendered. What does the parent do in this case? Does it:

  • block as well

The parent can go ahead and render without even waiting for the child, why should it? It can go right ahead and run updated once the child has rendered.

  • assume that the component is meant to be removed the DOM v-if until it is done loading

I am not sure I get this... but the answer is no, we don't assume it will be removed, it just needs to do something during mounted that has to block mounted during that time. There are a lot of use cases read above for that.

  • assume that the component is meant to be hidden until completed loading

This depends on the developer, why they are asyncing the mounted or any other hook for that matter.

  • Show a temporary element in its place?

That may not be the case at all. Again it depends with the developer what they intend to achieve. The point is, nothing should be straight jacketed. When, for instance, v-if was designed it wasn't because they conceived why someone would want to block the rendering of a component each time, and what they place instead and fool-proofed it. There are many things that can go wrong with v-if by developer design. You shouldn't worry what will be on screen during that time, that's not for the framework to worry about.

While I agree that the expectations around a dynamically loaded component are consistent, the behavior for the parent I don't think would be. In these cases, it would be IMO exposing the implementation of the child to the parent and force the parent to figure out what to do with that dynamic component. Instead how the data is loaded and the state of the child component should be up to the child. If it loads async then it needs some way to explain to Vue what should be rendered in its place (or not rendered). The best way to handle that is how the framework already works, rather than introducing a new complexity.

FYI: You agrees this needs to be implemented, however, it will introduce these complexities you are crying about and he feels that it may be done later when breaking changes are introduced rather than in Vue 3. The point being that he feels it is needed.

Further, I'm not totally following your argument though:

What you are saying is like adding v-if/v-clock/v-show features will encourage bad practices so we better fool-proof the framework by removing those features

While in this case, we can clearly see that introducing async components awaiting the mount or created will cause bad practices, it isn't clear that this does. Second, it is begging the question, even if those do cause bad practices, we should opt for fixing them, rather than using them as a justification for create new bad practices. If you know of bad practices that are being created by v-if, etc... I invite you share explicitly (in another issue of course) what the problem with those are, rather than trying to use that as a justification for a different discussion.

I merely pointed out those directives as an example of features that can be used incorrectly to block the rendering of a component similar to what you were saying about async.... Nothing wrong with them. So should we remove those directives just because someone can use "bad practices" to create components that show blank pages for a minute? Actually you don't see anybody doing that because it doesn't happen, unless you are trying to give an example of uttermost badness as in awful.

Look, the point is, if you can't see any use case yet, then don't say other people will use it badly and therefore it shouldn't be done. Bad practice is a matter of ignorance and someone that ignorant may never use these features altogether.

Someone asked this https://github.com/vuejs/vue/issues/7209#issuecomment-424349370 up there and nobody answered him as far as I saw. This is by far showing genuinely that Vue is legging behind on this part. This part of the architecture was designed when async wasn't a thing yet. So updating it to meet modern requirements for modern architectures is sure a good idea. Otherwise the rest is hackish and workarounds that need specific ways of doing it rather than do what's happening in the industry.

However, I am not sure yet, but looking at the new functional API at a glance seems like it may actually be possible to do this. Since it is functional it means one can do certain things they couldn't objectively do, like async lifecycle hooks.

Look, the point is, if you can't see any use case yet, then don't say other people will use it badly and therefore it shouldn't be done. Bad practice is a matter of ignorance and someone that ignorant may never use these features altogether.

Never made that point, I'm making the point that I want to perform async actions by default without ever blocking the component from rendering. It is unintuitive that performing async actions in an mounted or created block will cause the rendering of the component to be delayed. Assuming this was the case, I would instead see the complexity arise from how a consumer, who wants the current functionality would proceed. The argument so far isn't that what is being asked for, can't be done, it is that what is being asked for should be the default. You can already block the rendering of your component by switching the displayed template based on a v-if="loaded".

Right now to render without blocking the code looks like this:
Right now that code is:

<template>
  <div>  
    <spinner v-if="!loaded">
    <div v-else>
      ....
    </div>
</template>

<script>
  async created() { 
    await this.loadData();
    this.loaded = true;
  }
</script>

And to render with blocking looks the exact same way, since you can't actually block. Assuming the async created() actually blocked the component. Then we now have a separation of code. To display the spinner, we write:

<template>
  <div>  
    <spinner v-if="!loaded">
    <div v-else>
      ....
    </div>
</template>

<script>
  created() { 
    this.loadData().then(() => this.loaded = true);
  }
</script>

and just ignore rendering the component on the screen we write

<template>
  <div>  
    <div>
      ....
    </div>
</template>

<script>
  async created() { 
    await this.loadData();
  }
</script>

For what benefit does adding this simplification for blocking the rendering warrant making not blocking more complicated? I'm just not seeing it.

Dependency handling for components

@yyx990803 Please take a look at this, it's not perfect, but a complex use case scenerio nevertheless.

Ok here is a use case that could have been elegantly handled if lifecycle hooks had async ... await:
_I actually wished to do this in an app and got ugly code to achieve this. This is a very contrived example of coz_

I need component A to await for component B && C's mounted hooks to fire before mounting itself. So component A's mounted has to await its created hook, which triggered the mounting of component B && C that were waiting for the trigger _(which can actually do something prior to waiting)_. Thing is it's easier this way and much much cleaner and intuitive as everything stays in one place for the concerned component.

A emits a trigger event and listens for responses from B and C _(B and C wait for A's signal before continuing, then emit events once mounted)_ before continuing, simple. This is more like a dependency scenario for components without any extraneous data littered elsewhere for state management.

Main hosting component, all the components are loaded together, but wait for the right ones using events and async... await. It cares less what these children do, they order themselves.

<template>
  <div>
     ...
     <component-A/>
     <component-B/>
     <component-C/>
     ... other conent
  </div>
</template>
<script>
  import ComponentA...
  ...
  export default {
      components: { ComponentA... }
  }
</script>

component A controls the mounting of B and C as dependancies. The triggering could be done later in other hooks or even via a UX event of the same components. The following is just to show off the idea here.

<template>
  ...
</template>
<script>
  export default {
      async created() {
          // ...do something here before mounting thrusters, even await it
         this.root.$emit('mount-thrusters');
         await Promise.all([
            this.wasMounted('thruster-1-mounted'), 
            this.wasMounted('thruster-2-mounted')
         ]); 
      },
      mounted() {
        // will only run after components B and C have mounted
        ...
      },

     methods: {
       wasMounted(compEvent) {
          return new Promise( (resolve)=>this.root.$once(compEvent, ()=>resolve()));
       }
    }
  }
</script>

component B

<template>
  ...
</template>
<script>
  export default {
      async created() {
          // ...do something here, even await it, but happens at the same time as all components
         await new Promise( (resolve)=>this.root.$once('mount-thrusters', ()=>resolve()));
      },
     mounted() {
       this.root.$emit('thruster-1-mounted');
    }
  }
</script>

component C

<template>
  ...
</template>
<script>
  export default {
      async created() {
          // ...do something here, even await it, but happens at the same time as all components
         await new Promise( (resolve)=>this.root.$once('mount-thrusters', ()=>resolve()));
      },
     mounted() {
       this.root.$emit('thruster-2-mounted');
    }
  }
</script>

The code above can be simplified further by mixins seeing there are a lot of duplicate code snippets, I just wanted it to be clear. The wasMounted method can be canned in a mixin and used across all 3 components.

Here we can clearly see what each component is expecting without any other hackish or router code littered elsewhere to control the very same thing. It's very confusing to actually do this without this feature, believe me I have done this in an app.

This obviously gets rid of unnecessarily complex and unmaintainable code.

Now imagine this in a large App, with 32 thruster components that behave differently. You will only have about 3 points to mind, which are reducible to even 2 if you thrown in mixins.

Making things stay fresh

Of coz this is not only limited to mounted and created, but actually should work with all other lifecycle hooks. Imagine if this is in a shiny new beforeActivate/beforeUpdate hook. We could make the component await _activation/update_ and only _activate/update_ when refreshment is done each time the component is _activated/updated_; making sure things stay fresh.

The list is endless once this is implemented.

Look, the point is, if you can't see any use case yet, then don't say other people will use it badly and therefore it shouldn't be done. Bad practice is a matter of ignorance and someone that ignorant may never use these features altogether.

Never made that point, I'm making the point that I want to perform async actions by default without ever blocking the component from rendering. It is unintuitive that performing async actions in an mounted or created block will cause the rendering of the component to be delayed. Assuming this was the case, I would instead see the complexity arise from how a consumer, who wants the current functionality would proceed. The argument so far isn't that what is being asked for, can't be done, it is that what is being asked for should be the default. You can already block the rendering of your component by switching the displayed template based on a v-if="loaded".

Right now to render without blocking the code looks like this:
Right now that code is:

<template>
  <div>  
    <spinner v-if="!loaded">
    <div v-else>
      ....
    </div>
</template>

<script>
  async created() { 
    await this.loadData();
    this.loaded = true;
  }
</script>

And to render with blocking looks the exact same way, since you can't actually block. Assuming the async created() actually blocked the component. Then we now have a separation of code. To display the spinner, we write:

<template>
  <div>  
    <spinner v-if="!loaded">
    <div v-else>
      ....
    </div>
</template>

<script>
  created() { 
    this.loadData().then(() => this.loaded = true);
  }
</script>

and just ignore rendering the component on the screen we write

<template>
  <div>  
    <div>
      ....
    </div>
</template>

<script>
  async created() { 
    await this.loadData();
  }
</script>

For what benefit does adding this simplification for blocking the rendering warrant making not blocking more complicated? I'm just not seeing it.

This is Vue 101 bootcamp and there is nothing new there... it's not sufficient to cover the above cases for example. The idea here is to reduce complexity in the userland where Vue is actually used and avail easier to reason-about code.

The rendering here is not the issue, it's what is happening before the render that is actually of consequence. We want the freedom to do things before proceeding or rendering a component. Anyway, this also has nothing to do with blocking rendering at all. There are a lot of lifecycle hooks that Vue supports and these can actually be useful if there was a way to handle async code against other hooks. The idea is for Vue to respect async internally before going to the next hook function.

I'm really confused by this, instead of coupling B & C to A, I would move the code to "load A" into A.js and then have A.js update the "B.data" and "C.data". That way if the A.data changes for any reason the other components are automatically rerendered rather than trying to delegate
control from one component to another one. Even in the case shared, I would still decouple the data to render A from the component A. We have used a single class which contains methods like fetchData and hasInitialized for which the latter is a promise and the former resolves the latter.
Directly coupling the components together creates unexpected dependency trees which prevents the components from being reusable and allowing vue to rerender them correctly.

Alternatively, I would even emit the event directly to the parent of A, B, and C, and not on the global scope, and let the parent decide if to render B and C, i.e.

  <template>
    <a-component @rendered="showBandC = true" />
    <b-component v-if="showBandC" />
    <c-component v-if="showBandC" />
  </template>

What is it about A that in this case we would actually need to render before B and C renders. If there is stuff in the created() method, nothing prevents that from populating the store via a javascript class or using a store module. But the explicit use case would more helpful, i.e. what is the UX of the user story that can't be captured?

The idea is for Vue to respect async internally before going to the next hook function.

Sure that part makes sense, but I'm not sure why the example needs to be convoluted, why not just say something like this user story:

My component has both async beforeMount and async mounted, but the code in mounted is firing before the code in beforeMount is completed. How can we block mounted from firing before beforeMount is completed?

Which is sort of what the original request was, so the question that was brought forth in the second response I think is still relevant: https://github.com/vuejs/vue/issues/7209#issuecomment-350284784

Closing for now, but feel free to follow up with more concrete reasoning / use cases / implications.

Is there actually a valid use case to needing to block on previously executed lifecycle hooks, or is it correct for lifecycle hooks to synchronous. So far the discussion has been philosophical in nature (as good architecture discussions tend to do), but really the question is has there been an actual good reason to do this. I don't doubt for a second it is reasonable for the framework to await async code. I had the exact trouble in N other libraries that didn't do that or pass a callback (or pass a callback but not pass a callback to the callback). However, it is actually reasonable to have an async lifecycle hook or are the reasons on the result of trying to do something that "shouldn't be done"?

I.e. what happens when you attempt to unmount a component that hasn't finished being created, wow, that would be bad to be awaiting that still. Or destroying one that hasn't finished being mounted, I don't envy the implementor of that functionality.

I'm really confused by this, instead of coupling B & C to A, I would move the code to "load A" into A.js and then have A.js update the "B.data" and "C.data". That way if the A.data changes for any reason the other components are automatically rerendered rather than trying to delegate

That's complexity increased, bad practice. Try writing it here in full let's see what you mean, but to me you have just increased complexity by a lot.

control from one component to another one. Even in the case shared, I would still decouple the data to render A from the component A. We have used a single class which contains methods like fetchData and hasInitialized for which the latter is a promise and the former resolves the latter.

This is now coupling the components too much. We want these to work without the other except for A.

Directly coupling the components together creates unexpected dependency trees which prevents the components from being reusable and allowing vue to rerender them correctly.

In fact you are missing the point, these are loosely coupled to the extend that each can be used and maintained without affecting the other. In fact you can drop any of them multiple times anywhere without writing any more code in the parents outside of <component-x />, no v-if, vuex to manage its state nor anything else.

I coupled them A to B and C just to demonstrate, I could have split this nicely or just count how many components have responded then continue when a certain expected number is reached (2 in this case) eg:

 this.$root.$emit('thruster-mounted') // in B and C
// instead of 
 this.$root.$emit('thruster-1-mounted') // for B and 2 for C

// then count the responses and resolve once they are >= 2 in component A

Anyway, that's why I said Components Dependency Handling, this is desired and expected, but to be done in as little complexity as possible because the more complex it gets the more confusing it becomes.

Alternatively, I would even emit the event directly to the parent of A, B, and C, and not on the global scope, and let the parent decide if to render B and C, i.e.

I knew you were going to say this, but this is undesired. These components should not be controlled by anything else, hence I emphasised that the main component cares less what its kids do, they should remain independent. I want to be able to place them anywhere in the App and still make this same thing work. The moment you do what you are saying there, watch how everything breaks apart. But I can easily literally put components anywhere in the DOM tree without even flinching, and everything will just work. I can even have multiple copies of this and it will still work. All thanks to async ... await on lifecycles.

  <template>
    <a-component @rendered="showBandC = true" />
    <b-component v-if="showBandC" />
    <c-component v-if="showBandC" />
  </template>

What is it about A that in this case we would actually need to render before B and C renders.

We want each component to do unique work before it is mounted. But all components will only render when every other component has finished doing that work. That's the story here.
So good luck with state management and v-if's just to control this not to mention the actual data it will probably produce in the vuex store. You will end up having a lot of duplicate code written wherever the component is used. Let's say you have another component that hosts A and C only and in a different configuration? You have to write those v-if's, vuex state management to make that work. Do you see the problem? let me illustrate:

  // component Z totally different from foo, so we can't make this a component for reuse
  <template>
    <a-component @rendered="showBandC = true" />
    <c-component v-if="showBandC" />
  </template>
 ...
computed: {
showBandB() { ...vuex ...,
showBandC() { ...vuex ...,
}
  // component Foo totally unique, probably has other things it does
  <template>
    <b-component v-if="showBandC" />
    <c-component v-if="showBandC" />
  </template>
 ...
computed: {
// ok you could mixin this, fair, but complex nevertheless
showBandB() { ...vuex ...,
showBandC() { ...vuex ...,
}

..... and soo forth

instead of just using the components without ever doing anything in the parents like so:

  // component Z
  <template>
    <a-component />
    <b-component />
  </template>
  // component Foo
  <template>
    <b-component  />
    <c-component />
  </template>

... and so forth

If there is stuff in the created() method, nothing prevents that from populating the store via a javascript class or using a store module. But the explicit use case would more helpful, i.e. what is the UX of the user story that can't be captured?

Remember what I said about state management littered everywhere? We don't want that coz managing these components means we will be managing a lot things elsewhere, which is very complex instead of what I just did. Besides, it won't do what I want this to do; only mount when each component has completed what it is supposed to do with very little complexity.

The idea is for Vue to respect async internally before going to the next hook function.

Sure that part makes sense, but I'm not sure why the example needs to be convoluted, why not just say something like this user story:

My component has both async beforeMount and async mounted, but the code in mounted is firing before the code in beforeMount is completed. How can we block mounted from firing before beforeMount is completed?

The point is that we want things to happen before any of these components render and be usable without too much input outside of the components themselves. This is an actual thing I noticed I could have done better if this was available an app that we are making. I ended up writing code that has a lot of mixins, vuex and heavily coupled parents everywhere (using mixins) the components were used becoz this is missing. This is not some convoluted story, I am actually telling you what happened in a simple manner. We had to rethink how the UI was supposed to be designed and maintained. It got a little less interesting visually and a lot complex code-wise.

Do you see how many complex things you introduced into this simple setup that was just solved by that little async ... await feature in lifecycles?

@wparad

I.e. what happens when you attempt to unmount a component that hasn't finished being created, wow, that would be bad to be awaiting that still. Or destroying one that hasn't finished being mounted, I don't envy the implementor of that functionality.

This is where:

... requires a fundamental rethink/rewrite of the architecture to achieve, and can potentially break a lot of logic that relies on the synchronous nature of lifecycle hooks...

... part I believe was being mentioned by You. We need a way to handle these edge cases and probably more that this introduces. In other words our intents must work without crashing the app.

In all those cases I'd use timeouts to check or abort the wait, much like most async operations that may fail.

But if you look at the example I presented carefully, you will see what I mean. This could have solved a lot of things, without much code written anywhere. In fact our App could have been a lot better with a much small, defragmented and easy to grasp codebase if this were possible.
The thing about these kind of features is that you only see their importance once you hit a roadblock. I first came to this page when we had thought of many workarounds about this very example I just gave. I had not come to seek help, it was to lodge a feature request since we noticed it wasn't available in Vue.

Was this page helpful?
0 / 5 - 0 ratings