question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Bind vm instance to local async component registration

See original GitHub issue

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:

  1. provide behavioral differences for (grand)children
  2. 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 that created() 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:

  1. As @sirlancelot articulates here, the apparent difference between <component :is> and local component 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

  2. 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:closed
  • Created 5 years ago
  • Reactions:8
  • Comments:10 (3 by maintainers)

github_iconTop GitHub Comments

7reactions
sirlancelotcommented, Oct 16, 2018

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:

<!-- "compnent-split.vue" -->
<template>
  <div id="feature">
    <component :is="childFeature">
    <div>some other feature html</div>
  </div>
</template>

<script>
export default {
  inject: ["flavorOfChild"],
  computed: {
    childFeature() {
      const feature = this.flavorOfChild.name
      return () => import(`./features/${feature}.vue`)
    }
  }
}
</script>

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

import LoadingSpinner from "./components/loading.vue"

export default {
  // ...snipped
  computed: {
    childFeature() {
      const feature = this.flavorOfChild.name
      return () => ({
        component: import(`./features/${feature}.vue`),
        loading: LoadingSpinner
      })
    }
  }
}

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.

4reactions
yyx990803commented, Dec 6, 2018

Closing since there is a userland solution and binding this actually complicates caching and could potentially leads to hard to detect bugs.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Create and start a VM instance - Compute Engine
In the Google Cloud console, go to the VM instances page. Go to VM instances. Select your project and click Continue. Click Create...
Read more >
Component Registration - Vue.js
There are two ways to register components: global and local. Global Registration #. We can make components available globally in the current Vue...
Read more >
How To Handle Async Data Loading, Lazy Loading, and Code ...
React has a special component called Suspense that will display placeholders while the browser is loading your new component. In future versions ...
Read more >
Local variable in template for async pipe (angular 2+)
Inside the component you can name the variable whatever you like. ... <ng-container *ngIf="(vm$ | async)?.profile as profile"> <div> ...
Read more >
Testing - Spring
To provide Dependency Injection of test fixture instances. ... or an array of component classes that is used to configure the application.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found