Hay cuatro ejes de fricción: la gestión de estado (de Vuex a Pinia requiere repensar el flujo de datos, no solo la sintaxis), la obtención de datos (de asyncData/fetch a composables), el ecosistema (librerías de terceros sin soporte para Vue 3) y el cambio a la Composition API (convenciones de equipo, perder this, nuevas particularidades de reactividad). La migración debe ser incremental, usando Nuxt Bridge como paso intermedio.
1. De Vuex a Pinia
No es un buscar y reemplazar. El modelo mental cambia por completo:
// Nuxt 2: Vuex con mutations, módulos con namespace
// store/user.js
export const state = () => ({ user: null })
export const mutations = {
SET_USER(state, user) { state.user = user }
}
export const actions = {
async fetchUser({ commit }, id) {
const user = await this.$axios.$get(`/users/${id}`)
commit('SET_USER', user)
}
}// Nuxt 3: store de Pinia
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
async function fetchUser(id: number) {
user.value = await $fetch(`/api/users/${id}`)
}
return { user, fetchUser }
})Qué cambia:
- Las mutations desaparecen. Las acciones modifican el estado directamente.
- Los módulos con namespace se convierten en stores independientes que se importan entre sí.
this.$store.dispatch('user/fetchUser', id)pasa a seruseUserStore().fetchUser(id).- Los nombres de acciones en formato string se convierten en llamadas de función tipadas.
- El store ya no es un singleton global con estructura rígida, es un composable.
La fricción es organizativa: cada componente que usa this.$store o mapState/mapGetters necesita reescribirse.
2. Obtención de datos
Cada patrón de obtención de datos cambia:
// Nuxt 2: asyncData recibe un objeto context
export default {
async asyncData({ $axios, params, error }) {
try {
const user = await $axios.$get(`/users/${params.id}`)
return { user }
} catch (e) {
error({ statusCode: 404 })
}
}
}<!-- Nuxt 3: composables en script setup -->
<script setup>
const route = useRoute()
const { data: user, error } = await useFetch(`/api/users/${route.params.id}`)
</script>Qué cambia:
asyncDatayfetch(la opción de componente de Nuxt 2) no existen.- El objeto
context({ $axios, store, redirect, error }) desaparece. Cada capacidad es ahora un composable separado (useRoute,useRouter,navigateTo,useFetch). $axiosse reemplaza normalmente por$fetch(integrado en Nuxt 3 via ofetch).- El manejo de errores usa
createErroro el referrordeuseFetch.
3. Middleware
// Nuxt 2: basado en context
export default function ({ store, redirect }) {
if (!store.state.auth.loggedIn) {
redirect('/login')
}
}// Nuxt 3: basado en composables
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useAuth()
if (!loggedIn.value) {
return navigateTo('/login')
}
})El parámetro context desaparece por completo. Se usan composables en su lugar. redirect() pasa a ser navigateTo(). El middleware de servidor es ahora un concepto separado en server/middleware/.
4. Plugins
// Nuxt 2: inject en el context
export default function ({ app }, inject) {
inject('analytics', new Analytics())
}
// Uso: this.$analytics.track(...)// Nuxt 3: provide a través de nuxtApp
export default defineNuxtPlugin((nuxtApp) => {
const analytics = new Analytics()
nuxtApp.provide('analytics', analytics)
})
// Uso: const { $analytics } = useNuxtApp()Cada plugin que usaba inject necesita reescribirse. Los componentes que accedían a valores inyectados mediante this.$algo ahora usan useNuxtApp().
5. Ecosistema de terceros
Muchas librerías de Vue 2 / Nuxt 2 no sobrevivieron la transición:
| Librería | Estado |
|---|---|
vue-class-component | Descontinuada, sin equivalente en Vue 3 |
vue-property-decorator | Descontinuada |
vuetify@2 | Vuetify 3 existe, pero la migración tardó años |
@nuxtjs/axios | Reemplazado por $fetch integrado |
@nuxtjs/auth | Sin versión oficial para Nuxt 3; usa sidebase/nuxt-auth o implementación propia |
Módulos de nuxt-community | Algunos migraron, muchos abandonados |
Conviene auditar todas las dependencias al principio. Algunas tienen equivalentes para Nuxt 3, otras necesitan reemplazo y algunas requieren reimplementación personalizada.
6. Particularidades de reactividad en la Composition API
Los desarrolladores que vienen de la Options API se encuentran con nuevos problemas:
- Olvidar
.valueen los refs (el error más frecuente) - Desestructurar objetos
reactive()pierde la reactividad (hay que usartoRefs()) thisno existe en<script setup>- El comportamiento de
watches diferente (requiere fuente explícita frente a los watchers de cadena de la Options API) computeddevuelve un ref, no un valor simple
Estrategia de migración
El enfoque recomendado es incremental, no una reescritura total:
Nuxt Bridge primero: instala
@nuxt/bridgeen tu proyecto Nuxt 2. Esto te da el runtime de Vue 3 con las APIs de Nuxt 3 (useFetch,useState,defineNuxtPlugin) manteniendo el código existente funcionando.Migrar la gestión de estado: de Vuex a Pinia. Esto puede hacerse mientras se sigue en Bridge.
Migrar componentes de forma incremental: convierte de la Options API a
<script setup>un componente a la vez. Ambos estilos funcionan en paralelo.Migrar la obtención de datos: reemplaza
asyncData/fetchporuseFetch/useAsyncData.Migrar middleware y plugins: reemplaza los patrones de
contextpor composables.Migrar módulos: reescribe con
@nuxt/kitsi tienes módulos propios.Eliminar Bridge: cambia a Nuxt 3 completo, actualiza
nuxt.config.ts, ejecuta el pase de pruebas final.Eliminar APIs obsoletas:
$listeners, el bus de eventos$on/$off, los filtros,$set/$delete.
Las pruebas en cada paso son fundamentales. Añade tests end-to-end para los flujos críticos antes de empezar la migración, para tener una red de seguridad.