-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
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
Comments
So you want created () {
return new Promise(resolve => {
setTimeout(() => {
console.log('created')
resolve()
})
})
},
mounted () {
console.log('mounted')
} to display
? 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 Ideally, I want that line of code to be executed after the component has been created (so that @yyx990803 |
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 |
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 |
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 |
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.
If the lifecycle call in async/await flow, it would follow the order But currently I cannot get user info at 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:
|
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 So the Vue instance will call |
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 |
@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. |
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. |
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 |
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
In any case, |
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, |
@seanfisher Thank you, this is super helpful! |
Why not using an asynchronous component instead, which will be rendered only when asynchronous calls have returned? 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. |
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. |
What you are saying is like adding 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 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 Actually someone who will actually use an |
@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
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:
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 |
The parent can go ahead and render without even waiting for the child, why should it? It can go right ahead and run
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.
This depends on the developer, why they are asyncing the mounted or any other hook for that matter.
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,
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.
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 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. |
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 Right now to render without blocking the code looks like this: <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 <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 need component A to await for component B && C's 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 freshOf 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 The list is endless once this is implemented. |
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 Alternatively, I would even <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
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 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)
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 |
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.
This is now coupling the components too much. We want these to work without the other except for A.
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 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.
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
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. // 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
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 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? |
This is where:
... 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. |
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 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 |
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. |
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:
use it like this
Output as expected: created > beforeMount > mounted |
<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 I'm data hook |
I'm surprised that Vue still doesn't have this, as there's currently no way for components to manage their own cleanup. So, for example, say you have a Dialog element. You want that to show / hide with v-if (so that you don't get your DOM stuffed with dialog instances). Right now, you have to consumer of your dialog must wrap the dialog in a transition element, and then sync the transition length to the length of the "closing" animation of the dialog. There's no way for your library's dialog element to do something simple like:
Instead, Vue nukes the dialog and prevents any animation, at least at the point of component definition. So now you must tell the consumer of your library to:
At the very least, if adding async behavior to existing hooks is a problem, why not add async lifecycle hooks? |
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.
The text was updated successfully, but these errors were encountered: