Dynamic and async components made easy with Vue.js

Published on 2018/02/11 ‚óŹ Updated on 2018/09

Table of contents

I think this post will be most useful to advanced Angular/AngularJS developers that are still pondering what’s all the fuss about Vue.js —just like I was just a few weeks ago.

Nonetheless, I have included step-by-step instructions that will help beginners too.

So, in this —opinionated— tutorial, I’m hoping you’ll:

  • Realize just how ludicrously easy is to setup on-demand / lazy-load for Vue.js components.
  • Show you how to combine dynamic components + async loading for maximum effect!

Demo

You can find a live demo here.

Use cases

These are the use cases we will review in this tutorial:

  • The app router should only load code for sections of the app you actually visit.
  • You should be able to dynamically add/remove/swap components on a page.
  • Inside a section, the app should only load code for components that are actually rendered.
    A component might be declared an available for rendering, but its code should only load if you display it.

Prerequisites

Install Node.js, Yarn and vue-cli

Create a new Vue.js app

In a terminal, go to a directory of your choosing, then:

vue create dynamic-async-components-demo

Select Manually select features, leave defaults —babel, eslint— alone and also pick Router and CSS Pre-processors.

  • When asked Use history mode for router? choose: No.
  • For the CSS pre-processor pick: Stylus.
  • For the linter pick: ESLint + Standard config.
  • Accept defaults for everything else.

It’ll then proceed to install your project’s dependencies, and generate your first commit.

Adjust ESLint rules

Add this line to the rules key in the .eslintrc.js file:

'no-multiple-empty-lines': [2, { max: 2 }]

Add Pug templates support

Then add support for the Pug template engine:

yarn add pug pug-plain-loader --dev

Once it’s finished:

 cd dynamic-async-components-demo
 yarn serve

Check it out at: http://localhost:8080.

Asynchronous route loading

To showcase this feature let’s create a new SFC (Single File Component) —don’t you just love .vue files?

Create a dynamic-async-components-demo/src/views/Playground.vue file with this content:

<template lang="pug">
section.playground
  h1 Welcome to the component playground
</template>

Taking advantage of Webpack’s code chunking we can make our app’s initial rendering very fast by only loading the code we need at the beginning and loading everything else on-demand.

Let’s make our new Playground.vue component to load asynchronously.

Open the dynamic-async-components-demo/src/router.js file and modify it to look like this:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'


Vue.use(Router)


export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    },
    {
      path: '/playground',
      name: 'playground',
      component: () => import('@/views/Playground')
    }
  ]
})

To see the async loading in action, open the browser console (press F12) go to the Network tab and visit http://localhost:8080/.

Now visit http://localhost:8080/#/playground and observe how it makes a request for a 0.js file when you change the URL.

The good thing? The app will only make that request once, then it’ll be cached afterwards!

This is the line of code that makes all this possible:

component: () => import('@/views/Playground')

How hard is this for async loading of router components? Pretty damn easy I bet! :)

Dynamic component rendering

Another thing that is very easy to do in Vue.js just have a look for yourself and judge.

Defining new components

Let’s create three components to use on the Playground:

  • A Button component.
    dynamic-async-components-demo/src/components/MyButton.vue:

    <template lang="pug">
    button I'm a button
    </template>
  • A Header component.
    dynamic-async-components-demo/src/components/MyHeader.vue:

    <template lang="pug">
    h1 I'm a header
    </template>
  • A TextInput component.
    dynamic-async-components-demo/src/components/MyTextInput.vue:

    <template lang="pug">
    input(type="text" placeholder="I'm a text input")
    </template>

Static rendering

If you want to see your new components in action, modify the src/views/Playground.vue file to look like this:

<template lang="pug">
section.playground
  h1 Welcome to the component playground

  my-button
  my-header
  my-text-input
</template>


<script>
import MyButton from '@/components/MyButton'
import MyHeader from '@/components/MyHeader'
import MyTextInput from '@/components/MyTextInput'

export default {
  components: { MyButton, MyHeader, MyTextInput }
}
</script>

Then visit: http://localhost:8080/#/playground. You’ll see something like this:

Static Rendering

Dynamic rendering

What we’ll do in this section is present a <select> input as means to pick a component from a list and display it.

This can be accomplished through the powerful <component> element.

Modify the Playground.vue file to look like this:

<template lang="pug">
section.playground
  h1 Welcome to the component playground

  select(v-model="selectedComponent")
    option(
      v-for="(item, index) in componentList"
      :key="index"
      :value="item.component"
    ) {{ item.label }}

  hr
  .dynamic-component
    component(:is="selectedComponent")
</template>


<script>
import MyButton from '@/components/MyButton'
import MyHeader from '@/components/MyHeader'
import MyTextInput from '@/components/MyTextInput'

export default {
  data () {
    return {
      componentList: [
        {
          component: MyButton,
          label: 'A button'
        },
        {
          component: MyHeader,
          label: 'A header'
        },
        {
          component: MyTextInput,
          label: 'A text input'
        }
      ],
      selectedComponent: null
    }
  }
}
</script>


<style lang="stylus" scoped>
.dynamic-component
  border: 1px dashed red
  display: inline-block
  padding: 1em
</style>

Visit: http://localhost:8080/#/playground and pick A text input. You’ll see this:

Dynamic Rendering

Async loading + dynamic rendering

What do you think would take to enable async loading for the MyTextInput component above?

Well, you’ll only need to change Playground.vue to this:

<template lang="pug">
section.playground
  h1 Welcome to the component playground

  select(v-model="selectedComponent")
    option(
      v-for="(item, index) in componentList"
      :key="index"
      :value="item.component"
    ) {{ item.label }}

  hr
  .dynamic-component
    component(:is="selectedComponent")
</template>


<script>
import MyButton from '@/components/MyButton'
import MyHeader from '@/components/MyHeader'
// Line below is commented since we will be loading it asynchronously
// import MyTextInput from '@/components/MyTextInput'

export default {
  data () {
    return {
      componentList: [
        {
          component: MyButton,
          label: 'A button'
        },
        {
          component: MyHeader,
          label: 'A header'
        },
        {
          // Async loading!
          component: () => import('@/components/MyTextInput'),
          label: 'A text input'
        }
      ],
      selectedComponent: null
    }
  }
}
</script>


<style lang="stylus" scoped>
.dynamic-component
  border: 1px dashed red
  display: inline-block
  padding: 1em
</style>

Very easy, don’t you think?


You can verify it’s loading asynchronously by watching the Network tab in your browser console.
Then select the A text input option.
At that moment a request will be made to retrieve that component’s code!

Staying alive

The astute reader might have noticed that whatever you type in the MyTextInput component is lost when you switch to another component.

If you want to preserve on memory what’s on the dynamic components, all you have to do is surround the <component> element with <keep-alive> tags like this:

<template lang="pug">
section.playground
  h1 Welcome to the component playground

  select(v-model="selectedComponent")
    option(
      v-for="(item, index) in componentList"
      :key="index"
      :value="item.component"
    ) {{ item.label }}

  hr
  .dynamic-component
    keep-alive
      component(:is="selectedComponent")
</template>

Go try it out! Pick A text input, type something in, then pick something else, then return to A text input and you’ll see whatever you typed in before is still there. Awesome!

Some observations

Do you know what other thing is a testament to Vue’s power and flexibility?
That Vuetify is a more advanced, featureful and complete implementation of the Material Design Guidelines than Angular Material itself.


If you are an Angular developer, compare the stuff we did above with how you’d do lazy loading in Angular.

GitHub Repository

If you don’t want to type all that code, here you have a repo ready for cloning:
https://github.com/lobo-tuerto/dynamic-async-components-demo


— lt

Feedback & comments

Get in touch on Twitter

Or by good ol' email at adriandcs@gmail.com