Andrew Welch · Insights · #vuejs #vue-3 #frontend

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

A taste of Vue.js 3: API Changes, Async Components, and Plugins

This arti­cle takes you through changes that have to be made when mov­ing to Vue.js 3, cov­er­ing API changes, async com­po­nents, and adapt­ing exist­ing plugins

We’re cur­rent­ly in the plan­ning phase for a project, and are choos­ing the tech­nolo­gies that we’ll be using as the basis for it.

Vue.js will be amongst those tech­nolo­gies, but should we go with Vue 2 or Vue 3, which is cur­rent­ly still a beta?

It’s at that awkward stage where it could go either way

At the time of this writ­ing, Vue.js 3 is at ver­sion 3.0.0-beta 14, and is slat­ed for release Q2 2020. For now, it can be found at the vue­js/vue-next GitHub repo.

What we decid­ed to do was attempt to con­vert over the scaf­fold­ing we use in the nystudio107/​craft repo and detailed in the An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment article.

If every­thing went smooth­ly, then away we go… Vue.js 3 it is. If not, then we stick with Vue.js 2.

We were pri­mar­i­ly inter­est­ed in using the new Com­po­si­tion API, bet­ter Type­Script sup­port, and some oth­er key improve­ments in Vue.js 3.

But mostly, the version of Vue we picked would be in use for some time

This arti­cle dis­cuss­es the changes we need­ed to make to con­vert the scaf­fold­ing over. It shows some real-world sit­u­a­tions and anno­tates the changes we had to make to get the code up and run­ning on Vue.js 3.

This arti­cle does­n’t detail every change in Vue.js 3, for that check out the Vue 3 Tuto­r­i­al (for Vue 2 users) arti­cle and the New awe­some­ness com­ing in Vue.js 3.0 podcast.

Spoil­er alert: it went well, we’re using Vue.js 3 for this project!

Link Overview of the Changes

The changes we’re going to be mak­ing here are real­ly rel­a­tive­ly triv­ial. We have a sin­gle JavaScript entry point app.js file, and a Vue­Con­fet­ti component.

This skele­ton code is what I use for my scaf­fold­ing, because it’s nice to see some con­fet­ti to indi­cate that your code is work­ing as intended.

The app.js is just a shell that does­n’t do much of any­thing oth­er than load the Vue­Con­fet­ti com­po­nent, but the project does demon­strate some inter­est­ing things:

  • Changes need­ed to your package.json file
  • Changes need­ed to your web­pack config
  • The changes need­ed to instan­ti­ate a new Vue app
  • How to do web­pack dynam­ic imports of Vue 3 APIs
  • How to use async com­po­nents in Vue 3 using new Async Com­po­nent API
  • How we can adapt a Vue plu­g­in that assumes being able to glob­al­ly inject instance prop­er­ties via Vue.prototype

If you’re using vue-cli, there’s a vue-cli-plu­g­in-vue-next plu­g­in that will auto­mate some of the project con­ver­sion for you, but I want­ed to get my hands dirty.

If you’re inter­est­ed in see­ing all of the major changes in Vue.js 3, check out the Vue.js merged RFCs.

And now, with­out fur­ther ado… let’s get on with the show!

Link Package.json Changes

The first thing we need to do is con­vert over the package.json pack­ages to ver­sions that work with Vue.js 3.

Here are just the additions/​changes need­ed (not the com­plete package.json):

{
    "devDependencies": {
        "@vue/compiler-sfc": "^3.0.0-beta.2",
        "css-loader": "^3.4.2",
        "file-loader": "^6.0.0",
        "mini-css-extract-plugin": "^0.9.0",
        "vue-loader": "^16.0.0-alpha.3"
    },
    "dependencies": {
        "vue": "^3.0.0-beta.14"
    }
}

Link webpack config changes

Next up we need to make some very minor changes to the web­pack con­fig detailed in the An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment article.

We just need to make two changes in the webpack.common.js file, and we’re done.

First, we need to change how we import the Vue­Load­er­Plu­g­in:

const VueLoaderPlugin = require('vue-loader/lib/plugin');

To look like this:

const { VueLoaderPlugin } = require('vue-loader');

Next up we need to change what file we alias vue to, by changing:


        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        },

To look like this:


        alias: {
            'vue$': 'vue/dist/vue.esm-bundler.js'
        },

Link app.js Changes

House­keep­ing out of the way, now we can get into the actu­al changes in the JavaScript & Vue components.

Here’s what the skele­ton app.js looked like for Vue.js 2:

// Import our CSS
import styles from '../css/app.pcss';

// App main
const main = async () => {
    // Async load the vue module
    const { default: Vue } = await import(/* webpackChunkName: "vue" */ 'vue');
    // Create our vue instance
    return new Vue({
        el: "#page-container",
        components: {
            'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
        },
        data: {
        },
    });
};

// Execute async function
main().then( (vm) => {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

We have an async func­tion main() that awaits the promise returned by the web­pack dynam­ic import of the Vue con­struc­tor.

This pat­tern allows the main thread to con­tin­ue exe­cut­ing while web­pack han­dles dynam­i­cal­ly load­ing the vue chunk.

While this is some­what point­less in the skele­ton code, this type of dynam­ic import­ing allows for code split­ting that becomes ben­e­fi­cial from a per­for­mance point of view as our appli­ca­tion gets fleshed out.

Then we cre­ate a new View­Mod­el, adding in our async com­po­nent Confetti.vue (we’ll get to the com­po­nent in a bit).

Let’s have a look at the changes we need to make to this code to get it work­ing on Vue.js 3:

// Import our CSS
import styles from '../css/app.pcss';

// App main
const main = async () => {
    // Async load the Vue 3 APIs we need from the Vue ESM
    const { createApp, defineAsyncComponent } = await import(/* webpackChunkName: "vue" */ 'vue');
    // Create our root vue instance
    return createApp({
        components: {
            'confetti': defineAsyncComponent(() => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue')),
        },
        data: () => ({
        }),
    }).mount("#page-container");
};

// Execute async function
main().then( (root) => {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

The glob­al Vue con­struc­tor is gone in Vue.js 3, and instead we need to explic­it­ly import the func­tions from the Vue.js 3 API that we need.

In this case, we’re going to need createApp() to cre­ate our app instance, and we’ll need defineAsyncComponent() to uti­lize the new Async Com­po­nent API for using async components.

createApp() returns an app instance, which has an app con­text that is avail­able to all com­po­nents in the com­po­nent tree.

Unlike Vue.js 2, this app does­n’t auto­mat­i­cal­ly mount, so we call .mount("#page-container"), which returns the root com­po­nent instance, which mounts on the DOM ele­ment with the id page-container.

To get our async com­po­nent Confetti.vue work­ing, all we need to do is wrap the func­tion we used in Vue.js 2 with defineAsyncComponent().

Also of note is that data can no longer be an object, but rather needs to be a fac­to­ry func­tion that returns a data object. While you’d often do this in Vue.js 2 already, it’s now manda­to­ry in Vue.js 3.

If you want to learn more about some of these glob­al API changes, check out the Glob­al API Change RFC.

Link Confetti.vue changes

Now onto the all impor­tant Confetti.vue component! 🎉

The exist­ing code for the Confetti.vue com­po­nent looks like this, and is rough­ly a copy & paste of the exam­ple on the vue-con­fet­ti GitHub repo:

<template>
    <main>
    </main>
</template>

<script>
    import Vue from 'vue'
    import VueConfetti from 'vue-confetti'

    Vue.use(VueConfetti);

    export default {
        mounted: function() {
            this.$confetti.start({
                shape: 'heart',
                colors: ['DodgerBlue', 'OliveDrab', 'Gold', 'pink', 'SlateBlue', 'lightblue', 'Violet', 'PaleGreen', 'SteelBlue', 'SandyBrown', 'Chocolate', 'Crimson'],
            });
            setTimeout(() => {
                this.$confetti.stop();
            }, 5000);
        },
        methods: {}
    }
</script>

Unfor­tu­nate­ly, this did­n’t work out of the box on Vue.js 3, giv­ing us the error:

Uncaught TypeError: Cannot read property '$confetti' of undefined

So to fig­ure out what was wrong here, I had a look at the Vue plu­g­in VueConfetti we’re import­ing, which looks like this:

import Confetti from './confetti';

export { Confetti };

export default {
  install(Vue, options) {
    if (this.installed) {
      return;
    }
    this.installed = true;
    Vue.prototype.$confetti = new Confetti(options); // eslint-disable-line no-param-reassign
  },
};

The way plu­g­ins work is that they define an install() func­tion that is called to do what­ev­er they need to do to install them­selves, when Vue.use() is called.

In Vue.js 2, the glob­al Vue con­struc­tor is passed in as the first para­me­ter, but in Vue.js 3 we’d actu­al­ly be call­ing app.use(), and the first para­me­ter then becomes the app con­text, which is not a con­struc­tor, and thus has no .prototype.

Indeed, if we console.log() the first para­me­ter passed in via Vue.js 2, we’ll see the Vue constructor:

ƒ Vue (options) {
  if ( true &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

But a console.log() the first para­me­ter passed in via Vue.js 3, we’ll see the app context:

{_component: {…}, _props: null, _container: null, _context: {…}, …}
component: ƒ component(name, component)
config: (...)
directive: ƒ directive(name, directive)
mixin: ƒ mixin(mixin)
mount: (containerOrSelector) => {…}
provide: ƒ provide(key, value)
unmount: ƒ unmount()
use: ƒ use(plugin, ...options)
_component: {components: {…}, data: ƒ}
_container: null
_context: {config: {…}, mixins: Array(0), components: {…}, directives: {…}, provides: {…}}
_props: null
get config: ƒ config()
set config: ƒ config(v)
__proto__: Object

So okay, how can we fix this? The prob­lem is that Vue­Con­fet­ti is try­ing to inject a glob­al­ly shared instance prop­er­ty $confetti via Vue.prototype.$confetti, but don’t have a glob­al con­struc­tor in Vue.js 3, so .prototype isn’t a thing here.

One way would be to change the vue-confetti/index.js code to use the new app instance’s config.globalProperties to accom­plish the same thing, some­thing like:

app.config.globalProperties.$confetti = new Confetti(options);

c.f.: Attach­ing Glob­al­ly Shared Instance Properties

But this would require chang­ing the Vue­Con­fet­ti code via a fork/​pull request. While I’m not against doing this, I real­ized there was an eas­i­er way to accom­plish the same thing:

<template>
</template>

<script>
    import Confetti from 'vue-confetti/src/confetti.js';
    export default {
        data: () => ({
            confetti: new Confetti(),
        }),
        mounted: function() {
            this.confetti.start({
                shape: 'heart',
                colors: ['DodgerBlue', 'OliveDrab', 'Gold', 'pink', 'SlateBlue', 'lightblue', 'Violet', 'PaleGreen', 'SteelBlue', 'SandyBrown', 'Chocolate', 'Crimson'],
            });
            setTimeout(() => {
                this.confetti.stop();
            }, 5000);
        },
        methods: {}
    }
</script>

Here we change the Confetti.vue com­po­nent to direct­ly import 'vue-confetti/src/confetti.js' and assign the new Confetti() object to our local data state object, rather than hav­ing it be glob­al­ly available.

This feels a lit­tle nicer to me in gen­er­al, because there’s prob­a­bly no great rea­son for the $confetti object to be glob­al­ly avail­able, if we’re cre­at­ing a Confetti.vue com­po­nent that can nice­ly encap­su­late it.

Link Should you use Vue.js 3 now?

We decid­ed to use Vue.js 3 now, but should you?

I think much depends on how heav­i­ly you lean on third par­ty com­po­nents, plu­g­ins, and mixins.

The more code you’ll be writing yourself, the safer it is to use Vue.js 3 now

While all soft­ware always has issues, Vue.js 3 itself seems quite sol­id, and the first-par­ty pack­ages like Vuex and Vue-Router are com­ing along great.

There will like­ly be some lag in third par­ty pack­ages get­ting updat­ed for Vue.js 3, and some may nev­er be.

Thus whether to go with Vue.js 3 now real­ly depends on how much you rely on said third par­ty packages.

For us, the ben­e­fits are worth­while enough for us to begin learn­ing and using Vue.js 3 now.

Link Wrapping up

Hope­ful­ly this small dive into what it looks like updat­ing your code for Vue.js 3 is help­ful to you. While it is rel­a­tive­ly nar­row in scope, it does touch on some top­ics I had­n’t seen cov­ered else­where, at least not wrapped up in a neat package.

I’m excit­ed to explore Vue.js 3 fur­ther, and will very like­ly doc­u­ment more of my jour­ney learn­ing the new hot­ness in Vue.js 3.

Hap­py coding!