Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
A taste of Vue.js 3: API Changes, Async Components, and Plugins
This article takes you through changes that have to be made when moving to Vue.js 3, covering API changes, async components, and adapting existing plugins
We’re currently in the planning phase for a project, and are choosing the technologies that we’ll be using as the basis for it.
Vue.js will be amongst those technologies, but should we go with Vue 2 or Vue 3, which is currently still a beta?
It’s at that awkward stage where it could go either way
At the time of this writing, Vue.js 3 is at version 3.0.0-beta 14, and is slated for release Q2 2020. For now, it can be found at the vuejs/vue-next GitHub repo.
What we decided to do was attempt to convert over the scaffolding we use in the nystudio107/craft repo and detailed in the An Annotated webpack 4 Config for Frontend Web Development article.
If everything went smoothly, then away we go… Vue.js 3 it is. If not, then we stick with Vue.js 2.
We were primarily interested in using the new Composition API, better TypeScript support, and some other key improvements in Vue.js 3.
But mostly, the version of Vue we picked would be in use for some time
This article discusses the changes we needed to make to convert the scaffolding over. It shows some real-world situations and annotates the changes we had to make to get the code up and running on Vue.js 3.
This article doesn’t detail every change in Vue.js 3, for that check out the Vue 3 Tutorial (for Vue 2 users) article and the New awesomeness coming in Vue.js 3.0 podcast.
Spoiler 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 making here are really relatively trivial. We have a single JavaScript entry point app.js file, and a VueConfetti component.
This skeleton code is what I use for my scaffolding, because it’s nice to see some confetti to indicate that your code is working as intended.
The app.js is just a shell that doesn’t do much of anything other than load the VueConfetti component, but the project does demonstrate some interesting things:
- Changes needed to your package.json file
- Changes needed to your webpack config
- The changes needed to instantiate a new Vue app
- How to do webpack dynamic imports of Vue 3 APIs
- How to use async components in Vue 3 using new Async Component API
- How we can adapt a Vue plugin that assumes being able to globally inject instance properties via Vue.prototype
If you’re using vue-cli, there’s a vue-cli-plugin-vue-next plugin that will automate some of the project conversion for you, but I wanted to get my hands dirty.
If you’re interested in seeing all of the major changes in Vue.js 3, check out the Vue.js merged RFCs.
And now, without further ado… let’s get on with the show!
Link Package.json Changes
The first thing we need to do is convert over the package.json packages to versions that work with Vue.js 3.
Here are just the additions/changes needed (not the complete 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 webpack config detailed in the An Annotated webpack 4 Config for Frontend Web Development 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 VueLoaderPlugin:
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
Housekeeping out of the way, now we can get into the actual changes in the JavaScript & Vue components.
Here’s what the skeleton 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 function main() that awaits the promise returned by the webpack dynamic import of the Vue constructor.
This pattern allows the main thread to continue executing while webpack handles dynamically loading the vue chunk.
While this is somewhat pointless in the skeleton code, this type of dynamic importing allows for code splitting that becomes beneficial from a performance point of view as our application gets fleshed out.
Then we create a new ViewModel, adding in our async component Confetti.vue (we’ll get to the component in a bit).
Let’s have a look at the changes we need to make to this code to get it working 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 global Vue constructor is gone in Vue.js 3, and instead we need to explicitly import the functions from the Vue.js 3 API that we need.
In this case, we’re going to need createApp() to create our app instance, and we’ll need defineAsyncComponent() to utilize the new Async Component API for using async components.
createApp() returns an app instance, which has an app context that is available to all components in the component tree.
Unlike Vue.js 2, this app doesn’t automatically mount, so we call .mount("#page-container"), which returns the root component instance, which mounts on the DOM element with the id page-container.
To get our async component Confetti.vue working, all we need to do is wrap the function 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 factory function that returns a data object. While you’d often do this in Vue.js 2 already, it’s now mandatory in Vue.js 3.
If you want to learn more about some of these global API changes, check out the Global API Change RFC.
Link Confetti.vue changes
Now onto the all important Confetti.vue component! 🎉
The existing code for the Confetti.vue component looks like this, and is roughly a copy & paste of the example on the vue-confetti 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>
Unfortunately, this didn’t work out of the box on Vue.js 3, giving us the error:
Uncaught TypeError: Cannot read property '$confetti' of undefined
So to figure out what was wrong here, I had a look at the Vue plugin VueConfetti we’re importing, 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 plugins work is that they define an install() function that is called to do whatever they need to do to install themselves, when Vue.use() is called.
In Vue.js 2, the global Vue constructor is passed in as the first parameter, but in Vue.js 3 we’d actually be calling app.use(), and the first parameter then becomes the app context, which is not a constructor, and thus has no .prototype.
Indeed, if we console.log() the first parameter 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 parameter 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 problem is that VueConfetti is trying to inject a globally shared instance property $confetti via Vue.prototype.$confetti, but don’t have a global constructor 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 accomplish the same thing, something like:
app.config.globalProperties.$confetti = new Confetti(options);
c.f.: Attaching Globally Shared Instance Properties
But this would require changing the VueConfetti code via a fork/pull request. While I’m not against doing this, I realized there was an easier way to accomplish 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 component to directly import 'vue-confetti/src/confetti.js' and assign the new Confetti() object to our local data state object, rather than having it be globally available.
This feels a little nicer to me in general, because there’s probably no great reason for the $confetti object to be globally available, if we’re creating a Confetti.vue component that can nicely encapsulate it.
Link Should you use Vue.js 3 now?
We decided to use Vue.js 3 now, but should you?
I think much depends on how heavily you lean on third party components, plugins, and mixins.
The more code you’ll be writing yourself, the safer it is to use Vue.js 3 now
While all software always has issues, Vue.js 3 itself seems quite solid, and the first-party packages like Vuex and Vue-Router are coming along great.
There will likely be some lag in third party packages getting updated for Vue.js 3, and some may never be.
Thus whether to go with Vue.js 3 now really depends on how much you rely on said third party packages.
For us, the benefits are worthwhile enough for us to begin learning and using Vue.js 3 now.
Link Wrapping up
Hopefully this small dive into what it looks like updating your code for Vue.js 3 is helpful to you. While it is relatively narrow in scope, it does touch on some topics I hadn’t seen covered elsewhere, at least not wrapped up in a neat package.
I’m excited to explore Vue.js 3 further, and will very likely document more of my journey learning the new hotness in Vue.js 3.
Happy coding!