Bind vm instance to local async component registration
See original GitHub issueWhat problem does this feature solve?
I work for a large firm with many developers, and we are defining a strategy for reusability while still providing the capability to provide conditional behavior. In other words, the design needs the ability to:
- provide behavioral differences for (grand)children
- code-split these differences for the purpose of scaling
EDIT: See this comment for a valid use-case.
This can easily be solved with vue mixins
(or extends
) and props
(or inject
) for a single level hierarchy, but things get complicated when trying to create multiple levels of nesting with configuration injected from a grand ancestor, when accounting for the need to code-split.
v-if
is a nice pattern, but it requires the child component to still be loaded even if the condition will always be false. This is a non-option for us, because of the performance implications at-scale loading code that is never used. Therefore we have a need to conditionally split child components into separate bundles based on instance properties.
Vue provides the ability to allow behavioral differences with mixins
/extends
/props
/inject
, and also the ability to provide promises (and therefore create split points) for local component registration definitions. We have tried coming at this many different angles, but there is no apparent way to do both (without losing server-side rendering). More information can be found by reading the section on async components.
EDIT: It’s also worth mentioning that SEO is a factor. Our application is fully universal (isomorphic) for the purpose of SEO and TTI. We do selective client lazy-loading where SEO is not important, but the typical use-case for code splitting the javascript is for the purpose of performance at-scale.
EDIT: There is a way to do both. Thanks to the solution provided by @sirlancelot.
The pattern that we came up with to solve this business need looks like this:
<!-- @file feature.vue, n-th level from parent -->
<template>
<div id="feature">
<child/>
<div>some other feature html</div>
</div>
</template>
<script>
export default {
inject: ['flavorOfChild'],
components: {
child() {
if (this.flavorOfChild === 'flavor 2') {
return import('./flavor-2');
}
return import('./flavor-1');
}
}
}
</script>
The issue is, the vm is not actually bound to the component function, and therefore this
is undefined. We can, however, take advantage of the component instance lifecycle to create a reference to the instance:
let _this;
export default {
inject: ['flavorOfChild'],
created() {
_this = this;
},
components: {
child() {
if (_this.flavorOfChild === 'flavor 2') {
return import('./flavor-2');
}
return import('./flavor-1');
}
}
}
Although the above solution “works”, it has the following limitations:
- It’s a bit messy to manually manage
_this
created()
would otherwise not be needed.- this opens up the opportunity for possible unexpected behavior for components that may be instantiated multiple times if the instances are created in parallel with different values.
- async component registration is not documented as part of the lifecycle, so there is no confidence that this lifecycle will remain consistent between versions (and thus
_this
may not be defined if Vue changes source such thatcreated()
happens after async components are resolved)
This is the solution we will be going with despite the limitations. There is also perhaps another way to conditionally lazy load child components that we have not considered. We did, however, try to come up with every possible design to accomplish our overall goal and could not.
EDIT: There is another way. Thanks to the solution provided by @sirlancelot.
EDIT: I have created a post to the vue forum to explore different design options (the need for this github issue assumes there is no other possible design that will solve our business need).
What does the proposed API look like?
export default {
components: {
child() {
if (this.someCondition) {
return import('./a');
}
return import('./b');
}
}
}
EDIT: Here you can see it demonstrated in a simplified form.
EDIT: Here you can see it demonstrated with vanilla js, agnostic of vue.
This seems like a simple change given the fact that the instance exists when these component functions are executed, and it is already possible to manage the binding manually like in the earlier examples.
I’d happily submit a PR but I could not find the right spot in the code to make the change.
EDIT: Now that a solution to my use-case has been provided (by @sirlancelot), this issue remains open for two reasons:
-
As @sirlancelot articulates here, the apparent difference between
<component :is>
and localcomponent
registration is the caching.computed
properties are expected to change, where the component definitions will be cached forever. There may be some benefit since in this use-case, the values are “configuration” and will never change -
There may be some other use-case that could benefit from design opportunities opened up by the vm being bound to the local async component registration promise function
Issue Analytics
- State:
- Created 5 years ago
- Reactions:8
- Comments:10 (3 by maintainers)
Top GitHub Comments
Thank you for such a detailed description! I took some time to read this and think it over before providing my response below. I think Vue can solve your issue as-is but until a core dev can provide a response, it’s anyone’s guess.
I’ve solved the scenario you’re describing by using a computed property in a wrapper component. Using your example scenario I think you would write something like this:
The most important part to this is that the computed property is returning a Function that starts the import process. Webpack will place the code-split there. Nothing more will be downloaded except what is required to fulfill that request. The added benefit you also get is that if
flavorOfChild.name
is a reactive object and its value changes, your computed property will “just work” and the new component will be downloaded and displayed.You can take it a step further by returning an async loading component definition instead (as seen in Handling Loading State):
Overall, I think the idea behind the
components
object in a component definition is that those functions should remain stateless and pure because Vue will cache that result indefinitely. If your component definition depends on external state, a dynamic<component :is="">
is the best solution because it tells the person reading your code (or just future “you”) that something here depends on the state of something else.Update: Note that it’s not required to use the component name as the file. This is a structure that I use for my larger applications. You can structure the files however you like. The important piece to keep in mind is that you can return a function and pass that function directly to
<component :is="">
and let Vue handle the rest.Closing since there is a userland solution and binding
this
actually complicates caching and could potentially leads to hard to detect bugs.