Vue 2 to Vue 3
A checklist from rebuilding a restaurant management system in Nuxt 3, Vuetify 3 and Pinia.
In 2024 I rebuilt most of meromenu, a restaurant management system, from Vue 2 to Vue 3. Vue 2 reached end of life in December 2023, so it had to move. The full story is in Rebuilding a restaurant app in Vue 3. This page keeps the parts I'd reuse.
Rebuild, don't upgrade in place
We didn't upgrade the old code. A senior developer started a fresh Nuxt 3 project with Vue 3, Vuetify 3 and Pinia, and set up the foundations first:
- login
- a wrapper for API calls
- form validation helpers
- a shared data table
- route guards
Then the app moved over one page at a time. Each new page could be compared with the old one before moving on, and the old app stayed live the whole time.
Before you start
Read every migration guide up front
Vue, Vuetify, Nuxt, Pinia (if coming from Vuex), the router. Several of my bugs came from small API changes that were already documented. I read the guides when something broke. Read them first.
List every component you use from the UI library
For each one, check its section of the upgrade guide, even the ones you think you know. That's exactly where the next bug was hiding.
Read the shared components before using them
The foundations (data table, API wrapper, validation) are the new contract. Know what options they accept before working around them.
Gotchas I hit
In Vuetify 2, the data table's item slot gave you isExpanded as a
boolean. In Vuetify 3 it's a function you pass the row to.
<v-icon>
{{ isExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon><v-icon>
{{ isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>A function is always truthy, so the old check was always true and there was no error. The fix was one line; finding it took five days, because the code looked correct.
The shared data table appends its own query string for paging:
getData(prop.url + '?' + serializedParam);A page passed a URL that already had a query string, so the final URL had two
?. Everything after the second one, including offset and limit, was
read as part of the last filter's value, and paging broke.
<TheDataTable
:url="accountAPI.activityLog(org)"
:filter-params="{ action_by: '', action: '', app_name: 'Inventory' }"
/>The table already accepted filters as an object. Use the component's way of doing it.
When the code looks right but the app is wrong
Check what changed underneath it, one assumption at a time. In a migration, the library under your code is the first suspect.
Last updated on