How to update a Vue 2 project to Vue 3


As one of the first demos on our demo website, we built a product configurator: a single page app for configuring and ordering postcards. This was in 2019. Today, three years later, I’ll describe our new setup for this project after migrating it from Vue 2 to Vue 3. 

In this post I will cover the tooling (Vue CLI/Vite), describe my experience using the composition API with the script setup syntax, discuss composables and talk about a new state management tool: Pinia.

Vue 3 and Vite

Vue 3 was released in september 2020, but at that time, not all tooling was ready for this new version and it took another 18 months before Vue 3 became the default version. Still, though, not all libraries are ready for Vue 3 (e.g., Vuetify and Bootstrap-Vue at the moment of writing).

More or less in parallel with the new version of Vue, a new build tool was developed by Evan You (the creator of Vue), called Vite. This tool has become the de facto build tool for Vue 3 projects as it simply outperforms the Vue CLI, mainly because the latter runs on Webpack and takes quite some time to spin up and compile your code. Vite is, as promised, lightning fast and with dev tools maturing, it also became really nice to use.

However, when you start reading on the Vite or Vue docs, it is not immediately clear how to set up a new project. The instructions from Vite, to scaffold a new project are

npm create vite@latest

whereas the instruction from Vue 3 docs tell you to do this

npm init vue@latest

At first, this seemed a bit ambiguous to me, but after digging into the details a bit more, these two commands are actually rather similar. Since Vite is framework agnostic, it needs some information from you to start the scaffolding. So, when you use npm create vite@latest, you get a prompt asking you a bunch of questions. Afterwards, the project is set up by the tool. Alternatively, add some extra parameters to the create vite command to set up a Vue 3 project like this:

npm create vite@latest my-vue-app — –template vue

This command will scaffold a new Vue 3 app in the my-vue-app folder. It will be rather close to the project you get when running the init vue command.

Script ‘setup’ syntax

When you read tutorials online, you probably stumbled upon this terminology: script setup. It’s syntactic sugar which makes life easy and reduces boilerplate in your Vue single file components. For example, in the past we needed to import a component and then add it to the components option. With script setup, only a single import is required. Moreover, all top-level variables and functions are exposed and you can use them in your templates directly.

The down-side of this approach is that we need to import ref and reactive from the vue package in every component where we have (local) state, but I guess that is the price we pay for more flexibility. Moreover, using composables (see below) we can abstract this away if required.


By using composables, we can group functionality. One of the drawbacks in Vue 2 (with the options API) was that your data, methods, computed properties, watchers and mounting code was grouped by their type rather than functionality. The composition API lets you group all data and methods (and all other stuff you need) for a feature in the script part. On top of that, composables enable you to put such code in its own file to be included wherever you need it. That makes code reuse a lot simpler than before when we had to work with mixins.

This probably needs an example to make my point clear. The product configurator demo project is set up for two languages (NL and EN). The active language and all translations are managed in a store (more on that in the Pinia part of this post), and ultimately, we need the lang getter we defined in almost all components.

Instead of copying the import lines over and over again, I wrote a composable which does the heavy lifting. The individual components then import this composable and have access to the lang variable.


The Vue 2 version of this project leaned heavily on Vuex. As its successor, we embraced Pinia. The main difference between Vuex and Pinia is that the latter removed the rather verbose mutations. Pinia stores thus only have state, getters (derived from state) and actions. Another change is that Pinia advocates the use of many stores instead of a single store in Vuex. This way we can separate different parts of the state even better and only load the parts we need (the bundle uses code splitting per store).

Example: composable to use the i18n store

As promised, here is the example composable I created (see gist below). It is used by components that require access to the lang object containing all translations for fixed text strings in the app and uses the package vue-simple-i18n. Using the composable is a 2 step process: 

  1. import the function;
  2. execute the function

In code this reads:

import { useLang } from ~/composables/i18n.js
const lang = useLang()

And here is how the composable works under the hood:

  • Lines 1,2 import a helper (storeToRefs) from Pinia and the store itself. 
  • On line 4 we define the named export useLang. This is a function. 
  • Line 5 creates an instance of the store.
  • On line 6 we use the storeToRefs method from Pinia, to get the lang getter from the store. We can’t simply use destructuring to retrieve the getter. From the Pinia docs:
    In order to extract properties from the store while keeping its reactivity, you need to use storeToRefs(). It will create refs for every reactive property. This is useful when you are only using state from the store but not calling any action.
    This is exactly what we need and with this function we can pretend we destructure the store to retrieve the getter lang
  • Finally, on line 7 we return the variable lang.

Hence, upon execution of the useLang() function imported from the composable, we end up with the lang object in the (parent) component.


In this post I wrote down my remarks for updating a Vue 2 project to Vue 3. I also switched my scaffolding tool (Vue CLI => Vite) and for state management I moved from Vuex to Pinia.

All in all I can say that the update went quite well for this small project. I like the structure of the new script setup syntax for the composition API and the ability to abstract things by using composables.

Cover image by Markus Winkler on Unsplash

Mijn Twitter profiel Mijn Facebook profiel
Pim Hooghiemstra Webdeveloper and founder of PLint-sites. Loves to build complex webapplications using Vue and Laravel! All posts
View all posts by Pim Hooghiemstra

Leave a Reply

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