Dynamic and async components made easy with Vue.js

Published on 2018 / 02 / 11

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!

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

These are the Node.js related packages and versions —and then some— I’ll be using for this tutorial:

git --version #git version 2.16.1
nvm --version #0.33.8
node --version #v.9.5.0
npm --version #5.6.0
yarn --version #1.3.2
vue --version #2.9.3

If you don’t have Yarn installed, you can do so with: npm install -g yarn.
Or check out this setup guide on how to install Node.js in Ubuntu.

If you don’t have vue-cli installed yet, you can do so with: yarn add global vue-cli.
Test it with vue --version.

Creating a new Vue.js app

We’ll be using vue-cli with the webpack template.

Let’s create a new Vue.js app:

vue init webpack dynamic-async-components-demo

Accept all defaults, except when asked between NPM/Yarn; select Yarn.

After finishing the installation process, let’s initialize a Git repository for this app and make our first commit.

cd dynamic-async-components-demo/
git init
git add .
git commit -m "Initial commit"
yarn dev

You can see your brand new app working at: http://localhost:8080.

Asynchronous route loading

Let’s create a new Single File Component (a .vue file) —don’t you just love this?— to showcase this feature.
Create a dynamic-async-components-demo/src/components/Playground.vue file with this content:

<template>
  <section class="playground">
    <h1>Welcome to the component playground!</h1>
  </section>
</template>

<script>
export default {
  name: 'Playground'
}
</script>

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/index.js file and modify it to look like this:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/playground',
      name: 'Playground',
      component: () => import('@/components/Playground')
    }
  ]
})

Now would be a good time to save our progress:

git add .
git commit -m "Add Playground.vue to router and load it async"

To see the async load in action open the browser console (press F12) and go to the Network tab.

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

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

The line of code making all this possible is this one:

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

How hard is this for async loading of router components eh? :)

Dynamic component rendering

This is so easy in Vue.js… just have a look for yourself and judge.

Defining new components

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

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

    <template>
      <button>I'm a button</button>
    </template>
    
    <script>
    export default {
      name: 'Button'
    }
    </script>
  • A Header component.
    dynamic-async-components-demo/src/components/dynamic/Header.vue:

    <template>
      <h1>I'm a header</h1>
    </template>
    
    <script>
    export default {
      name: 'Header'
    }
    </script>
  • A TextInput component.
    dynamic-async-components-demo/src/components/dynamic/TextInput.vue:

    <template>
      <input type="text" placeholder="I'm a text input"/>
    </template>
    
    <script>
    export default {
      name: 'TextInput'
    }
    </script>

Static rendering

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

<template>
  <section class="playground">
    <h1>Welcome to the component playground!</h1>

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

<script>
import Button from '@/components/dynamic/Button'
import Header from '@/components/dynamic/Header'
import TextInput from '@/components/dynamic/TextInput'

export default {
  name: 'Playground',
  components: {
    'my-button': Button,
    'my-header': Header,
    'my-text-input': TextInput
  }
}
</script>

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

Static Rendering

Let’s save our progress:

git add .
git commit -m "Add Button, Header, TextInput components and display them"

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 using the powerful <component></component> element.

Modify the Playground.vue file to look like this:

<template>
  <section class="playground">
    <h1>Welcome to the component playground!</h1>

    <select v-model="selectedComponent">
      <option
        v-for="(component, index) in componentList"
        :key="index"
        :value="component"
      >
        {{ component.name }}
      </option>
    </select>

    <hr>
    <component :is="selectedComponent"></component>
  </section>
</template>

<script>
import Button from '@/components/dynamic/Button'
import Header from '@/components/dynamic/Header'
import TextInput from '@/components/dynamic/TextInput'

export default {
  name: 'Playground',
  data: function () {
    return {
      componentList: [Button, Header, TextInput],
      selectedComponent: null
    }
  }
}
</script>

Visit: http://localhost:8080/#/playground, pick TextInput. You’ll see this:

Dynamic Rendering

Let’s save our progress:

git add .
git ci -m "Dynamic component rendering"

Async loading + dynamic rendering

What would it take to enable async loading for the TextInput component above?

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

<template>
  <section class="playground">
    <h1>Welcome to the component playground!</h1>

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

    <hr>
    <component :is="selectedComponent"></component>
  </section>
</template>

<script>
import Button from '@/components/dynamic/Button'
import Header from '@/components/dynamic/Header'
// Comment out the line below, since we will be loading it asynchronously
// import TextInput from '@/components/dynamic/TextInput'

export default {
  name: 'Playground',
  data: function () {
    return {
      componentList: [
        { label: 'Button', component: Button },
        { label: 'Header', component: Header },
        {
          label: 'TextInput',
          // Async loading!
          component: () => import('@/components/dynamic/TextInput')
        }
      ],
      selectedComponent: null
    }
  }
}
</script>

Easy, isn’t it?


Save your progress:

git add .
git commit -m "Load TextInput asynchronously"

You can verify it’s loading asynchronously by watching the Network tab in your browser console and selecting the TextInput option. At that moment a request will be made to retrieve this component’s code!

Staying alive

The astute reader might have noticed that whatever you type in the TextInput 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></component> element with <keep-alive></keep-alive> tags like this:

<keep-alive>
  <component :is="selectedComponent"></component>
</keep-alive>

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


Let’s do our final commit for this tutorial:

git add .
git ci -m "Preserve dynamic components state in memory"

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.

I was going all over its components and API the other day, drooling and laughing at the sheer number of elements they already have. :)


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 the code, just clone the repo at:
https://github.com/lobo-tuerto/vuejs-dynamic-async-components-demo


— lt

Feedback & comments

Get in touch on Twitter

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