🧱 Migration Guide: Vue2/Nuxt2/Vuetify2 to Vue3/Nuxt3/Vuetify3

As technology evolves, so must our projects. With Vue 2 support coming to an end on December 31st, 2023, it becomes imperative for us to migrate our projects to the latest Vue 3, Nuxt 3, and Vuetify 3.

In this blog, I will share my experience and insights gained while migrating the project based on an existing codebase, from tackling Nuxt 3 updates to addressing Vue 3 breaking changes and adapting to Vuetify 3. I navigated through numerous hurdles and discovered effective solutions, as a result, I will also share the challenges encountered and the strategies employed.

Background

Before going ahead with the migration, I extensively researched various technical blogs and almost all of them recommended migrating the code after creating a new project, cause there might be too many errors🐞 and could not figure out which module was causing the error or if it was something else.
Frame 1 _3_.png
But after weighing the pros and cons, I made the decision to upgrade the project based on our old codebase, mainly because our codebase already uses the composition api syntax by @nuxt/composition-api using TypeScript, and have also introduced auto-imports. It proved to be a very bold decision💥, as I was not able to validate the changes I made for quite some time, and in fact I did not actually get the project up and running until after I had completed most of the nuxt 3 and vue 3 migrations. But fortunately, I did succeed 🎉.
Frame 4 _1_.png

Before Migration

  1. Learn about Nuxt 3
  • It allows to experience TypeScript with zero configuration.
  • Nuxt 2 uses webpack 4 and Babel, Nuxt 3 switched to Vite (or webpack 5) and esbuild. Vite is the default choice but you can still use webpack 5.
  • It uses Nuxi for Command line interface. It is like the Vue CLI but for Nuxt.
  • It uses Nitro as the server engine, offering cross-platform support for Node.js, browsers, service workers and more.
  • It uses Nuxt Kit that provides users with a new flexible module development experience with cross-version compatibility.
  • It uses Nuxt Bridge to ease the migration from Nuxt 2.
  • Prerequisites Node.js - v16.10.0 or newer
  1. Learn Vue 3 Composition API
  • As Nuxt 3 is built on Vue 3, understanding the changes and enhancements in Vue 3 is essential. Pay attention to the Composition API, which offers a more flexible and powerful way to organize and manage your code.
  1. Learn about TypeScript
  • Nuxt 3 is written in TypeScript, and having a basic understanding of TypeScript will be beneficial. Being able to read and understand TypeScript code will help you navigate through Nuxt 3’s source code and address any potential issues that may arise during the migration process.
  • Migrating your code to TypeScript is highly recommended due to its enhanced type safety, improved developer experience with autocompletion and type inference, early detection of errors during development, robust tooling support, and its ability to maintain code scalability and readability as projects grow in complexity. TypeScript adds clarity and structure to the codebase, ultimately leading to more reliable and maintainable applications.
  1. Learn about Modules
  • Nuxt 3 requires ECMAScript modules (ESM), which may require converting CommonJS modules (CJS) used in your project. Learn about the differences between these module systems and how to migrate your modules to ESM.
  1. Try to set up a demo project
  • 👍👍👍 Having a demo project before migration is an excellent idea, especially when dealing with complex and large-scale projects. Here’s an example demo project including Vue 3, Nuxt 3, Vuetify 3, Pinia, pwa and firebase authentication: 👉 github repo, see more details in this blog.
  • It allows you to experiment and isolate specific packages or modules, helping you identify the source of issues more effectively. When facing challenges in the migration process, the demo project serves as a safe playground to troubleshoot and find solutions without impacting the main project.
  • This experimental approach fosters a deeper understanding of potential pitfalls and ensures a smoother migration for the main project, ultimately saving time and minimizing risks.

Resources

📖 Keeping the migration guide open and referring to it throughout the process will help you avoid common pitfalls and ensure a smoother transition.

Vue 3

Nuxt 3

Vuetify 3

Migration Plan

0. Abstract reusable components

  • Abstract recurring code patterns into reusable components.

1. Nuxt 3 Migration

  • Update Nuxt 3 configurations.
  • Adapt the codebase to address breaking changes introduced in the latest version.

2. Package Upgrades and Replacements

  • Upgrade package versions to their Vue 3-compatible equivalents.
  • Identify and replace packages that may not be compatible with Vue 3 or have alternative solutions available.

3. Integration of New Configurations

  • Integrate the updated router, i18n, pinia, pwa, firebase, and other configurations to align with Nuxt 3.

4. Vue 3 Migration

  • Begin the migration by updating the project’s codebase to work with Vue 3.
  • Rewrite all components using the “script setup” syntax introduced in Vue 3.

5. Test Project Setup

  • Attempt to run the project.
  • Address any issues or conflicts that may arise during the testing phase.

6. Vuetify 3 Migration

  • Follow the migration guide to adjust the code syntax of each component.
  • Integrate the latest theme configuration.
  • Explore alternative solutions for components that have not been fully migrated yet.

7. Style Restoration

  • Restore any customized styles or appearance adjustments to maintain the project’s original look.

Nuxt 3 Changes Checklist

full-logo-green-dark.png
To get the project set up as quickly as possible, I focused on the nuxt 3 migration firstly. Here is a checklist outlining the major changes required. Nuxt 3 introduces several significant changes and improvements that require careful consideration during the migration process. In this section, I will present a checklist detailing the key modifications needed to adapt the project to Nuxt 3.

Configuration
  1. nuxt.config
  • Migrate to the defineNuxtConfig function.
  • Migrate router.extendRoutes to the new pages:extend hook.
  1. Modules
  • Move all buildModules into modules.
  • Check for Nuxt 3 compatibility of modules. [we will do it later]
  1. TypeScript
  • Add "extends": "./.nuxt/tsconfig.json" to tsconfig.json.
Auto Imports
  1. Nuxt Auto-imports
  • Nuxt auto-imports functions and composables to perform data fetching, get access to the app context and runtime config, manage state or define components and plugins.
  1. Vue Auto-imports
  • Vue 3 exposes Reactivity APIs like ref or computed, as well as lifecycle hooks and helpers that are auto-imported by Nuxt.
Meta Tags
  • In nuxt.config, rename head => meta.
  • Access the component state with head => useHead.
Plugins and Middleware
  1. Plugins
  • Migrate plugins to use the defineNuxtPlugin helper function.
  • Remove entries in nuxt.config plugins array that are at the top level of plugins/ folder.
  1. Route Middleware
  • Migrate route middleware to use the defineNuxtRouteMiddleware function.
  • Reference route middleware using definePageMeta.
  • File name with extension .client and .server.
Pages and Layouts
  1. Layouts
  • Replace <Nuxt /> with <slot />.
  • Use definePageMeta to select the layout used by the page.
  • Move ~/layouts/_error.vue to ~/error.vue.
  1. Pages: Dynamic Routes
  • Replace _id with [id] to define a dynamic route parameter.
  • Replace _.vue with [...slug].vue to define a catch-all route.
Runtime Config
  • Add environment variables to the runtimeConfig property of the nuxt.config.
  • Migrate process.env to useRuntimeConfig.
  • Exposing Runtime Config using runtimeConfig.public.
Build Tooling
  • Remove @nuxt/typescript-build and @nuxt/typescript-runtime from dependencies and modules.
  • Remove any unused babel dependencies.
  • Remove any explicit core-js dependencies.
  • Migrate require to import.
Server
  • Any files in ~/server/api and ~/server/middleware will be automatically registered, so you can remove them from serverMiddleware array.

Vue 3 Changes Checklist

Vue 3 comes with several breaking changes compared to Vue 2, which may necessitate adjustments to the project's existing codebase. In this section, I will provide a checklist of these breaking changes and the corresponding solutions or workarounds.
v-model
  1. replace .sync with v-model.
    nuxt2: <ChildComponent :title.sync="pageTitle" />
    nuxt3: <ChildComponent v-model:title="pageTitle" />
  2. for all v-models without arguments, make sure to change props and events name to modelValue and update:modelValue respectively.
key
  1. no longer recommend using the key attribute on v-if/v-else/v-else-if branches, since unique keys are now automatically generated on conditional branches if you don’t provide them.
  2. when using <template v-for> with a child that uses v-if, the key should be moved up to the <template> tag.
v-if vs. v-for Precedence
  1. If used on the same element, v-if will have higher precedence than v-for.
    nuxt2: v-for would take precedence.
    nuxt3: v-if would take precedence.
v-bind Merge Behavior
  1. Order of bindings for v-bind will affect the rendering result.
    template: <div id="red" v-bind="{ id: 'blue' }"></div>
    result: <div id="blue"></div>
    template: <div v-bind="{ id: 'blue' }" id="red"></div>
    result: <div id="red"></div>
v-on.native modifier removed
  1. The .native modifier for v-on has been removed.
  2. Remove all instances of the .native modifier.
  3. Ensure that all your components document their events with the emits option.
Render Function API
  1. h is now globally imported.
  2. No domProps, the entire VNode props structure is flattened.
Slots Unification
  1. this.$slots now exposes slots as functions.
  2. this.$scopedSlots is removed.
  3. Replace all this.$scopedSlots occurrences with this.$slots in 3.x.
  4. Replace all occurrences of this.$slots.mySlot with this.$slots.mySlot().
Inline Template Attribute
  1. Support for the inline-template feature has been removed.
$children
  1. $children instance property has been removed and is no longer supported.
$listeners removed
  1. $listeners has been removed.
  2. Event listeners are now part of $attrs.
  3. Remove all usages of $listeners.
Events API
  1. $on, $off and $once instance methods are removed.
  2. Component instances no longer implement the event emitter interface.

What’s New in Vue 3

Async Components
  • defineAsyncComponent helper method that explicitly defines async components.
  • component option renamed to loader.
  • Loader function does not inherently receive resolve and reject arguments and must return a Promise.
emits Option
  • For components that re-emit native events to their parent, this would now lead to two events being fired.

Vuetify 3 Changes Checklist

vuetify-logo-v3-light 1 _1_.png
It is essential to address the changes in Vuetify 3. However, it is important to note that Vuetify 3 is still under development, and some components may not be fully available yet. Here’s a checklist of the changes and updates.

A => B means A has been renamed to B.

v-expansion-panel
  1. v-expansion-panel-header => v-expansion-panel-title.
  2. v-expansion-panel-content => v-expansion-panel-text.
v-menu
  1. offset-y, offset-x props => offset prop
v-img
  1. contain has been removed and is now the default behavior. Use cover to fill the entire container.
v-tabs
  1. v-tab-item has been removed, use v-window-item.
Input components
  1. validate-on-blur prop has been renamed to validate-on="blur".
  2. Variant props filled/outlined/solo have been combined into variant prop.
v-select/v-combobox/v-autocomplete
  1. v-model not present in items will now be rendered instead of being ignored.
  2. item-text => item-title
  3. The item slot will no longer generate a v-list-item component automatically, instead a props object is supplied with the required event listeners and props.
v-list
  1. v-list-item-icon and v-list-item-avatar have been removed, use v-list-item with icon or avatar props, or use append or prepend slot.
  2. v-list-item-content has been removed, use CSS grid for layout now instead.
  3. v-subheader => v-list-subheader.
v-form
  1. validate() now returns a Promise<FormValidationResult> instead of a boolean.
v-checkbox/v-radio/v-switch
  1. input-value => model-value.
v-btn/v-btn-toggle
  1. fab is no longer supported.
  2. flat, outlined, text, plain props have been combined into variant prop.
Layout
  1. stateless, clipped, clipped-right and app props have been removed from v-navigation-drawer, v-app-bar and v-system-bar.
v-data-table
  1. Should add items-per-page-text="Rows per page:" to change the text.
  2. item-key => add id to items.
  3. In items array text => title, value => key.
  4. Slot expanded-item => expanded-row ,header => columns.

Modules Compatibility

Ensuring the compatibility of third-party modules and plugins is crucial for a seamless transition. By upgrading, removing, or replacing modules as needed, we can maintain the project’s functionality and avoid potential conflicts.
For the modules named start with @nuxt, you can search for it here.

  1. Modules Removed
  • @nuxtjs/eslint-config-typescript removed
  • @nuxtjs/composition-api removed
  • @nuxt/typescript-build removed
  • @nuxtjs/axios removed
  • @nuxt/types removed
  • vue-i18n removed
  • vuexfire removed
  • vue-server-renderer removed
  • vue-template-compiler removed
  1. Modules Replaced
  • vue-chartjs => vue-chart-3
  • vue-apexcharts => vue3-apexcharts
  • vue2-perfect-scrollbar => vue3-perfect-scrollbar
  • vue-jest => @vue/vue3-jest guide
  • pinia-plugin-persistedstate => pinia-plugin-persistedstate/nuxt guide
  • @nuxtjs/pwa => @vite-pwa/nuxt guide
  1. Modules Upgraded
  • nuxt => ^3.5.2
  • vue => ^3.3.4
  • pinia => ^2.1.3
  • vuetify => ^3.3.3 guide
  • @nuxtjs/i18n guide
  • @vueuse/nuxt => ^10.1.2 guide
  • @pinia/nuxt => ^0.4.11

Challenges and Solutions

During the migration, I encountered several challenging issues that were not readily addressed in the official documentation. To find solutions, I proactively sought assistance from various sources, including the GitHub repository issues, the official Discord community, and Stack Overflow. In some cases, I even delved into the source code to gain a deeper understanding. Here are some of the questions I encountered and the corresponding answers I discovered.

❓ Refactoring Options API to Composition API

While the Options API remains functional in Vue 3, leveraging the Composition API offers significant advantages, especially for larger codebase. The Composition API is a built-in feature in Vue 3 and is also available in Vue 2 through the @vue/composition-api plugin.
Frame 2 _3_.png
Let’s see a basic example to understand the difference of coding structure between Options API and Composition API, the specific code images reference is from here: Arcana Network Blog.

1. Code in the Options API
Example 1 using Options API

2. Code in the Composition API
Example 1 using Composition API

3. Simplified the above code by using the <script setup> syntax
Example 1 using Composition API with script setup

❓ How to inject in context in Nuxt 3

In Nuxt 2, we used to use useContext to access the Nuxt context within the composition API, such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// plugins/hello.js
// Inject $hello(msg) in Vue, context and store.
export default ({ app }, inject) => {
inject('hello', msg => console.log(`Hello ${msg}!`))
}

// nuxt.config.js
export default {
plugins: ['~/plugins/hello.js']
}

// HelloWorldComponent.vue
// $hello can be accessed from context.
export default {
mounted() {
this.$hello('mounted') // Prints 'Hello mounted!'
},
asyncData({ app, $hello }) {
$hello('asyncData')
}
}

In Nuxt 3, we can access runtime app context within composables, components and plugins, such as:

1
2
3
4
5
6
7
8
9
10
11
// plugins/hello.js
export default defineNuxtPlugin(() => {
const nuxtApp = useNuxtApp() // access runtime nuxt app instance
nuxtApp.provide('hello', (name) => `Hello ${name}!`) // provide helpers
})

// HelloWorldComponent.vue
// useNuxtApp (on the server) only works during setup
// inside Nuxt plugins or Lifecycle Hooks.
const nuxtApp = useNuxtApp()
console.log(nuxtApp.$hello('name')) // Prints "Hello name!"
❓ How to use dynamic components in Vue 3

In Vue 2, we define the dynamic components in the components property, such as:

1
2
3
4
5
6
7
8
export default {
components: {
MyComponent: () =>
process.client
? import("my-component")
: Promise.resolve({ render: (h) => h("div") })
}
}

In Vue 3, we can use defineAsyncComponent function, which accepts a loader function that returns a Promise.

1
const MyComponent = defineAsyncComponent(() => import('my-component'))
❓ How to use labs components in Vuetify 3

1. Way 1: Import and bootstrap v-data-table in your component:

1
2
3
<script setup>
import { VDataTable } from 'vuetify/labs/VDataTable'
</script>

2. Way 2: Make the component available globally by importing it in Vuetify plugin.

1
2
3
4
5
6
7
8
9
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as labsComponents from 'vuetify/labs/components'
export default createVuetify({
components: {
...components,
...labsComponents,
},
})
❓ How to define themes in Vuetify 3

Define the themes in plugins/vuetify file, light and dark are pre-installed in Vuetify 3, and you can change the color of them as following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// plugins/vuetify.ts
export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({
ssr: true,
components,
directives,
themes: {
light: {
colors: {
something: '#fff'
},
dark: false
},
dark: {
colors: {
something: '#000'
},
dark: true
}
}
}
})
nuxtApp.vueApp.use(vuetify)
return {
provide: {
vuetify
}
}
})

To get the ‘something’ color under colors, you can do like this:

1
2
3
4
.your-class-name {
background: rgb(var(--v-theme-something))
color: rgba(var(--v-theme-on-something), 0.9)
}

To change the theme by clicking, you can do like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<v-btn @click="toggleTheme">
toggle theme
</v-btn>
</div>
</template>

<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
const toggleTheme = () => {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
}
</script>

❓ How to migrate useFetch from @nuxt/composition-api to Nuxt 3

In Nuxt 2, I use useFetch API from @nuxt/composition-api, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div v-if="fetchState.pending">
something
</div>
</div>
</template>

<script>
import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api'
import axios from 'axios'
export default defineComponent({
setup() {
const name = ref('')
const { fetchState } = useFetch(async () => {
name.value = await axios.get('https://myapi.com/name')
})
},
})
</script>

In Nuxt 3, there are many replacements, such as useAsyncData and useFetch that are auto-imported in Nuxt 3 project.

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<div v-if="pending">
something
</div>
</div>
</template>

<script setup>
const { pending, data: name, error, refresh } = useAsyncData('myapi', () => $fetch('https://myapi.com/name'))
</script>

What’s more, we can define options in useAsyncData including lazy, transform, watch. See more here.

❓ Error: v-on with no argument expects an object value

I got this warning in console when page first rendered, and it led to the problem that the menu doesn’t show when button is clicked, same problem for v-tooltip. After diving into the Vuetify 3 documentation, I found the solution in the upgrade guide, there’s a general change for components:

  • Activator slots work slightly different.
  • Replace #activator={ attrs, on } with #activator={ props }, then remove v-on="on" and replace v-bind="attrs" with v-bind="props".
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // before
    <v-tooltip>
    <template #activator="{ on, attrs }">
    <span v-on="on" v-bind="attrs">{{ title }}</span>
    </template>
    </v-tooltip>

    // after
    <v-tooltip>
    <template #activator="{ props }">
    <span v-bind="props">{{ title }}</span>
    </template>
    </v-tooltip>
    Related issue: here
❓ Error: Transition renders non-element root node that cannot be animated

Just keep there is only one root element.

1
2
3
4
5
6
7
8
9
10
11
<template>
<div></div>
<div>can not work</div>
</template>

<template>
<div>
 <div></div>
 <div>can work now</div>
</div>
</template>
❓ Error: Cannot read _leaveCb property

This error happened when using watch to refresh the data and assign a value to template. Both of these give an error, but disappeared if remove watch:

  1. watch(props, refresh);.
  2. add watch: [props] within useAsyncData.

I have tried all the solutions mentioned in similar issues, but only one of them works for me: Add <ClientOnly> around the component.

Related issues: nuxt issue, vue-router issue.

❓ Error: localStorage is not defined in nuxt server-side

When you use SSR you don’t have access to the browser storage, localStorage can only work on client side when process.client is true.

Lessons Learned

Frame 5 _1_.png

Conclusion

The migration from Vue 2/Nuxt 2/Vuetify 2 to Vue 3/Nuxt 3/Vuetify 3 has been a challenging but highly rewarding journey. By choosing to upgrade the project from its existing codebase, I was able to preserve valuable configurations, logic, and functionality, saving time and effort that would have been required to start from scratch.

Throughout the migration process, I have navigated through various aspects, including Nuxt 3 changes, Vue 3 breaking changes, Vuetify 3 updates, and package adjustments. By leveraging the official documentation, seeking support from my mentor 💪, and also the discussion form the community like GitHub, StackOverflow, and Discord, I overcame obstacles and found solutions to intricate problems.

Migrating a project of this scale required patience, perseverance, and attention to detail. At times, I faced moments of frustration and uncertainty 😢, but with determination and the guidance of my mentor, I stayed focused on the goal 🎯. The adoption of TypeScript in the Nuxt 3 project further enhanced code safety and maintainability, ensuring a more robust codebase. 🔝

As I progressed through each migration phase, I carefully integrated new versions, refactored components using the Composition API, and verified functionality through rigorous testing. This meticulous approach ensured a successful transition to Vue 3 and Nuxt 3, unleashing the full potential of their features.

This migration project has taught me valuable lessons in staying adaptable, seeking support, problem-solving, and the importance of continuous learning to keep our projects at the forefront of web development. 👍 And thanks again for my mentor R.J. for his unwavering support, guidance, and belief in my abilities, without which this achievement would not have been possible. I’m truly grateful for his time and encouragement throughout this remarkable journey. 😄


References:

🔍 Check out the demo project in github.
📮 If find any errors, please feel free to discuss and correct them: biqingsue@[google email].com.