The Benefits of the New Vue 3 App Initialization Code

Written by Mikhail Kuznetsov

In this article, we will start by looking at how the app initialization code works in Vue 2 apps. Then we’ll see what drawbacks it has and how those are eliminated with the new initialization syntax used in the 3rd version of the Vue framework.

Let’s start with the current way of doing initialization in Vue 2. Usually, in the src/main.js file, we’re bootstrapping the app by calling a new Vue as a constructor for creating an application instance.

import Vue from "vue";
import App from "./App.vue";
import router from './router'
import SomeComponent from '@/components/SomeComponent.vue'
import SomePlugin from '@/plugins/SomePlugin'

const vue2AppCopy = new Vue({
  router,
  render: h => h(App)
});

vue2AppCopy.component('SomeComponent',SomeComponent);
vue2AppCopy.use(SomePlugin);

vue2AppCopy.$mount('#app')

This application instance will be serving all the logic throughout the lifetime of our SPA. That all works well, and for about 3 years, we’ve been using this syntax to bootstrap our Vue apps.

However, in Vue 3 the initialization code syntax has changed. Let’s have a look at the new syntax first and then take a look at the benefits of using it.

New Vue 3 createApp method

In Vue 3, we have a dedicated createApp function to do just that. The createApp function takes a root component (App.vue) as an argument and returns a Vue app instance. So the simplest app initialization will look like this:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

The Vue app instance returned by the createApp is also called an application context object. This object can be used to further add more functionality to the app during the bootstrapping process. Here’s a more advanced initialization code example:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import SomeComponent from '@/components/SomeComponent.vue'
import SomePlugin from '@/plugins/SomePlugin'

const myV3App = createApp(App)
myV3App.component('SomeComponent', SomeComponent)
myV3App
.use(SomePlugin)
.use(router)
// add more functionality to myV3App

// now we're ready to mount
myV3App.mount('#app')

Compared to V2, not much has changed when it comes to adding extra logic, such as plugins and components, right?

You can find a full overview of the supported methods in the Vue 3 documentation.

That's good, but there’s one small but important change - we use a dedicated function, not the new Vue instance:

//v2
const vue2App = new Vue({}) // Working with the main Vue instace
//v3
const myV3App = createApp(App).mount('#app') // Create a copy of the Vue instance

So why is using that new dedicated createApp function better than using the new Vue constructor?

Benefits of the Vue 3 initialization code

In the Vue 2 app initialization code, we used the Vue object imported from the library to create this and all other new app instances.

With this approach, it was impossible to isolate some of the functionality to only one of the Vue instances since the Vue apps were still using the same Vue object imported from the library.

To demonstrate this, let’s look at the following example - as you can see, both vue2AppOne and vue2AppTwo will have access to a directive called myDirective:

Vue.directive('myDirective', {
    /* ... */
})

Vue.component({
  /* ... */
})

const vue2AppOne = new Vue(/**/).mount('#app1')
const vue2AppTwo = new Vue(/**/).mount('#app1')

It might not be so common to create multiple Vue apps in a website or application.

Learn Vue.js 3 With Vue School

But with a project growing in size, being developed by separate teams, and the popularity of frontend microservices in mind you may at some point find yourself doing this as well.

And then it will be close to impossible to get another Vue instance with different capabilities using the v2 syntax.

The new syntax in Vue 3 allows us to make the configuration of each app as an individual custom object, since the apps are configured using a dedicated function (createApp) to create the independent instances.

The new architecture gives us the possibility to have two or more isolated Vue instances that do not share any features by default, even if they are created in one file.

However, if you want to share some functionality between 2 instances - you can! In the following example, vue3AppOne and vue3AppTwo share the LocalePlugin but not the SearchInputComponent.

const config = {/* some global config */}

const vue3AppOne = Vue.createApp(config)
vue3AppOne.component('SearchInput', SearchInputComponent)
vue3AppOne.use(LocalePlugin)

const vue3AppTwo = Vue.createApp(config)
vue3AppTwo.use(LocalePlugin)

To demonstrate this behavior we’ve created a code repository with 2 simple Vue 3 instances that do not share components and directives thanks to the new createApp syntax. Have a look at it to play with it locally.

In the companion repository, we initialize two Vue 3 apps in 2 different containers on one page template, see public/index.html.

        <div id="header-app"></div>
        <div id="main-app"></div>

One app will serve as a relatively simple header markup, while the other will be capable of using the router and working with the store.

With the Vue 3 syntax, we can easily separate them in the initialization code in the src/main.js file:

import { createApp } from 'vue'
import App from './App.vue'
import Header from './Header.vue'
import router from './router'
import store from './store'

createApp(App)
  .use(store)
  .use(router)
  .mount('#main-app')

createApp(Header)
  .mount('#header-app')

If you run the app using vue serve you should be able to see both parts working on one page. In the console output you will see that the Main app has access to vue-router and the store using VueX while the Header app does not.

  created () {
    console.log('Hello from Main app')
    console.log('Main app router', this.$route)
    console.log('Main app store:', this.$store)
  }

A more straightforward testing setup

If you are using vue-test-utils (version < 2.0.0) for writing test for your Vue 2 components you may have come across the cases when you need to use the createLocalVue method to avoid polluting the global Vue instance.

We have the same potential issues in our test scenarios as we have in Vue 2 apps. We pollute the global Vue instance when we add components, plugins, etc., and they’re all shared with every available Vue instance.

To work around this issue, we have to use createLocalVue, which (you guessed it) creates a new local Vue instance, that is isolated.

import { createLocalVue, mount } from '@vue/test-utils'
import MyPlugin from '@/plugins/MyPlugin'

const localVueForTest = createLocalVue()
localVueForTest.use(MyPlugin)

mount(Component, {
  localVueForTest
})

This is no longer an issue in Vue 3, since all app extensions: plugins, mixins, and global components do not mutate the global Vue instance.
So when using vue-test-utils (version >=2.0.0) with Vue 3, the app init code in the test file will look like this:

import { createStore } from 'vuex'
import { mount } from '@vue/test-utils'
import App from '@/App'
import MyPlugin from '@/plugins/MyPlugin'

const wrapper = mount(App, {
  global: {
    plugins: [MyPlugin]
  }
})

Summary

In this article, we looked at the differences between the initialization code for Vue 3 and Vue 2 apps, as well as the benefits of the new approach.

Learn Vue.js 3 With Vue School

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

Abstracting your dependencies

Abstracting your dependencies