Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Await async component lifecycle hooks #7209

Closed
RashadSaleh opened this issue Dec 8, 2017 · 55 comments
Closed

Await async component lifecycle hooks #7209

RashadSaleh opened this issue Dec 8, 2017 · 55 comments

Comments

@RashadSaleh
Copy link

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.

@posva
Copy link
Member

posva commented Dec 8, 2017

So you want

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

to display

created
mounted

?

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

@yyx990803
Copy link
Member

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.

@RashadSaleh
Copy link
Author

@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?

@RashadSaleh
Copy link
Author

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.

@posva
Copy link
Member

posva commented Dec 8, 2017

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.

@RashadSaleh
Copy link
Author

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.

@RashadSaleh
Copy link
Author

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).

@posva
Copy link
Member

posva commented Dec 8, 2017

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

@RashadSaleh
Copy link
Author

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.

@RashadSaleh
Copy link
Author

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.

@sj82516
Copy link

sj82516 commented Jan 22, 2018

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

@manast
Copy link

manast commented Apr 3, 2018

a hackish workaround for what is worth:

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

@fifman
Copy link

fifman commented Apr 13, 2018

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
        }
    }
}

@darren-dev
Copy link

Is this not a thing yet?

@breadadams
Copy link

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

Is working for me (as @fifman mentions).

@darren-dev
Copy link

@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

@breadadams
Copy link

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

@darren-dev
Copy link

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

@Lord-Y
Copy link

Lord-Y commented Aug 27, 2018

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

@seanfisher
Copy link

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.

@darren-dev
Copy link

@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.

@smolinari
Copy link

smolinari commented Sep 23, 2018

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

@darren-dev
Copy link

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.

@smolinari
Copy link

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

@RashadSaleh
Copy link
Author

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).

@smolinari
Copy link

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

@sjmcdowall
Copy link

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!!

@464bb26bac556e85b6fd6b524347b103
Copy link

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.

@garyo
Copy link

garyo commented Nov 26, 2018

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?

@ReinisV
Copy link

ReinisV commented Apr 30, 2019

@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.

@cederron
Copy link

cederron commented Apr 30, 2019

@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

@wparad
Copy link

wparad commented May 7, 2019

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.

@lenjoseph
Copy link

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!

@baldychristophe
Copy link

baldychristophe commented May 10, 2019

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')
})

@emahuni
Copy link

emahuni commented Jul 10, 2019

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.

@robob4him
Copy link

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.

@wparad
Copy link

wparad commented Jul 17, 2019

@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.

@robob4him
Copy link

@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.

@gthomas2
Copy link

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.

@emahuni
Copy link

emahuni commented Sep 30, 2019

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.

@wparad
Copy link

wparad commented Sep 30, 2019

@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
Copy link

emahuni commented Sep 30, 2019

@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 #7209 (comment) 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.

@wparad
Copy link

wparad commented Sep 30, 2019

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.

@emahuni
Copy link

emahuni commented Sep 30, 2019

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.

@emahuni
Copy link

emahuni commented Sep 30, 2019

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.

@wparad
Copy link

wparad commented Sep 30, 2019

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: #7209 (comment)

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.

@emahuni
Copy link

emahuni commented Sep 30, 2019

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?

@emahuni
Copy link

emahuni commented Sep 30, 2019

@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.

@klausXR
Copy link

klausXR commented Jan 24, 2021

I've been playing around with an idea to achieve dynamically imported global plugins and this seems like a possible solution.

Vue plugins are a nice way to abstract functionality, however, once you have many of them, they increase the size of your bundle.

For example, you might add a date plugin ( like moment or dayjs ) to help you format dates, but might only need this in 2 out of 20 components - it is dead code in the remaining ones.

So, the alternatives may be to use functions instead of plugins and import them in the files that you want, making use of code-splitting to address the bundle size issue, but this does not give you the flexibility of plugins and adds more boilerplate for the more complex cases.

So, I was thinking I could add a single global plugin, which allows you to register any amount of lazily loaded plugins as dependencies of components, with an api similar to this

export default {
    name: "Post",

    props: {
        post: Object
    },

    // List of plugins required for this component to work
    dependsOn: [ "date" ],

    template: `
        <div>
            <h3 v-text="post.title"></h3>
            <p v-text="post.content"></p>
            <span>$date.format(post.createdAt)<span>
        </div>
    `
}

// Plugin to make it work

const LazyPlugins = {
    install(Vue) {
        Vue.mixin({
            async beforeCreate() {
                if (this.$options.dependsOn) {
                    // Use require to dynamically load and register the plugins,
                    // if they havent been registered yet
                }
            }
        })
    }
}

However, in order for this to work properly, I would need a way to defer the instantiation of the component until this method resolves, so that I don't get type errors when rendering.

Is there a good way to accomplish this? Or a whole other solution to the problem

@mzihlmann
Copy link

what about triggering an event when all created hooks are finished? then at least you could add your custom listener to that event. This would be a non-breaking change and already alleviate the situation.

My use case is that often I want watchers not to trigger on initialization, which I usually solve by setting the initialized variable to true in the next trick after the created hook and rewriting the watchers to check whether the component is already initialized or not. This is cumbersome. But if I try to move that logic to a mixin I utterly fail, because there is no way to know when an async created hook finished.

@thongxuan
Copy link

thongxuan commented Apr 22, 2021

HI guys, I jumped to this problem today and did a small trick to delay the life cycle hooks of Vue component. Note: use the @AsyncHook hook before any other decorators.

I created a decorator like this:

import { ComponentOptions } from "vue";
import { Vue } from "vue-property-decorator";

export const AsyncHook = () => (target: any): any => {
    const oldCreated = target.prototype.created || function () {};
    target.prototype.created = function () {
        oldCreated.call(this).then(() => {
            (this as any).createdResolver();
        });
    };

    const oldBeforeMount = target.prototype.beforeMount || function () {};
    target.prototype.beforeMount = function () {
        ((this as any).createdPromise as Promise<void>).then(() => {
            oldBeforeMount.call(this).then(() => {
                (this as any).beforeMountResolver();
            });
        });
    };

    const oldMounted = target.prototype.mounted || function () {};
    target.prototype.mounted = function () {
        ((this as any).beforeMountPromise as Promise<void>).then(() => {
            oldMounted.call(this);
        });
    };

    const newTarget = class extends target {
        private createdPromise: Promise<void>;
        private createdResolver: any;

        private beforeMountPromise: Promise<void>;
        private beforeMountResolver: any;

        constructor(...args: any[]) {
            super(args);

            this.createdPromise = new Promise((resolve) => {
                this.createdResolver = resolve;
            });
            this.beforeMountPromise = new Promise((resolve) => {
                this.beforeMountResolver = resolve;
            });
        }
    };

    //-- new target must inherit prototype from old target, otherwise there will be no hooks in new target
    newTarget.prototype = target.prototype;

    return newTarget;
};

use it like this

import { Component, Vue } from "vue-property-decorator";
import { AsyncHook } from "./decorator";

@Component({})
@AsyncHook()
export default class Child1 extends Vue {
    private async DelaySome(timeMs: number) {
        await new Promise((resolve) => setTimeout(resolve, timeMs));
    }

    async created() {
        await this.DelaySome(3000);
        console.log("created");
    }

    async beforeMount() {
        await this.DelaySome(1000);
        console.log("beforeMount");
    }

    async mounted() {
        await this.DelaySome(500);
        console.log("mounted");
    }
}

Output as expected: created > beforeMount > mounted

@aliviadav
Copy link

<script> export default { setup() { console.log("I'm setup hook"); }, data() { console.log("I'm data hook"); return { stateOfBob: "sleeping", }; }, computed: { test: function () { return "I'm computed hook"; }, }, beforeCreate() { console.log("I'm beforeCreate hook"); console.log("Bob is currently ", this.stateOfBob); console.log("computed hook is returning ", this.test); }, }; </script>

Output:

I'm setup hook

I'm beforeCreate hook
Bob is currently undefined
computed hook is returning undefined

I'm data hook

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests