-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
Description
What 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