Vue 3 y Nuxt (2026): Composition API, Pinia, SSR y Vapor Mode
Guía técnica profunda sobre Vue.js 3 y Nuxt.js: Composition API, Options API, gestión de estado con Pinia, Nuxt 3 SSR/SSG/ISR, Vue Router, patrones de componentes (slots, provide/inject, teleport), testing con Vitest, Vue DevTools, transiciones y animaciones, y Vite. Basada en la construcción de webgym-spa, una aplicación Nuxt white-label que sirve múltiples marcas de gimnasios.
Índice de Contenidos
- 1. Composition API vs Options API
- 2. Gestión de Estado: De Vuex a Pinia
- 3. Nuxt 3: SSR, SSG e ISR
- 4. Vue Router y Navigation Guards
- 5. Patrones de Diseño de Componentes
- 6. Transiciones y Animaciones
- 7. Testing con Vitest
- 8. Vue DevTools
- 9. Vite: La Herramienta de Build
- 10. Vue 3.5, Vapor Mode y Nuxt 4
1. Composition API vs Options API
La Composition API de Vue 3 reemplaza la organización basada en objetos de la Options API (data, computed, methods, watch) con un enfoque basado en funciones. La lógica se agrupa por preocupación funcional en lugar de por tipo de opción. Un componente que gestiona búsqueda y paginación antes dispersaba código relacionado entre data, computed, methods y watch; con la Composition API, cada preocupación se convierte en una función composable.
Options API: El Enfoque Clásico
La Options API organiza la lógica del componente por tipo de opción. Sigue siendo completamente soportada en Vue 3 y es una opción válida para componentes más simples. Sin embargo, a medida que los componentes crecen, la lógica relacionada se fragmenta entre diferentes opciones, dificultando el mantenimiento.
// Options API: logic organized by option type
export default {
data() {
return {
query: '',
results: [] as Member[],
page: 1,
isSearching: false,
};
},
computed: {
activeCount(): number {
return this.results.filter(m => m.status === 'active').length;
},
},
watch: {
query(newVal: string) {
this.search(newVal); // search logic here
},
page() {
this.search(this.query); // but pagination resets search too
},
},
methods: {
async search(q: string) {
this.isSearching = true;
this.results = await memberApi.search(q, this.page);
this.isSearching = false;
},
},
mounted() {
this.search('');
},
};
useMemberSearch composable, making it easier to read, test, and reuse across components.El Sistema de Reactividad
La reactividad de Vue 3 está construida sobre Proxies de ES6 (reemplazando Object.defineProperty de Vue 2). ref() envuelve un valor primitivo en un contenedor reactivo (acceso vía .value). reactive() hace un objeto entero reactivo (sin necesidad de .value). computed() crea valores derivados que se cachean y actualizan automáticamente. watch() y watchEffect() manejan efectos secundarios cuando los valores reactivos cambian.ref() wraps a primitive value in a reactive container (access via .value). reactive() makes an entire object reactive (no .value needed). computed() creates derived values that cache and update automatically. watch() and watchEffect() handle side effects when reactive values change.
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { useMemberSearch } from '@/composables/useMemberSearch';
import { usePagination } from '@/composables/usePagination';
import type { Member, MemberFilters } from '@/types';
// Composables: encapsulated, reusable reactive logic
const { query, results, isSearching, search } = useMemberSearch();
const { page, pageSize, total, totalPages, goToPage } = usePagination();
// Local reactive state
const filters = reactive<MemberFilters>({
status: 'all',
plan: null,
sortBy: 'name',
});
const selectedMember = ref<Member | null>(null);
// Computed: derived values with automatic caching
const activeCount = computed(() =>
results.value.filter(m => m.status === 'active').length
);
const hasFilters = computed(() =>
filters.status !== 'all' || filters.plan !== null
);
// Watch: react to filter changes
watch(filters, (newFilters) => {
goToPage(1); // Reset pagination on filter change
search(query.value, newFilters);
}, { deep: true });
// Lifecycle
onMounted(async () => {
await search('', filters);
});
</script>
Composables: El Corazón de la Reutilización de Código
Un composable es una función que usa la Composition API de Vue para encapsular y reutilizar lógica con estado. A diferencia de los mixins (que tenían colisiones de nombres y dependencias implícitas), los composables tienen entradas y salidas explícitas, seguimiento de dependencias claro y soporte completo de TypeScript. Cada composable es un módulo autocontenido con su propio estado reactivo, propiedades computadas y watchers.
// composables/useMemberSearch.ts
import { ref, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import type { Member, MemberFilters } from '@/types';
import { memberApi } from '@/api/members';
export function useMemberSearch(debounceMs = 300) {
const query = ref('');
const results = ref<Member[]>([]);
const isSearching = ref(false);
const error = ref<Error | null>(null);
const performSearch = useDebounceFn(async (q: string, filters?: MemberFilters) => {
isSearching.value = true;
error.value = null;
try {
results.value = await memberApi.search(q, filters);
} catch (e) {
error.value = e as Error;
results.value = [];
} finally {
isSearching.value = false;
}
}, debounceMs);
// Auto-search when query changes
watch(query, (q) => performSearch(q));
function search(q: string, filters?: MemberFilters) {
query.value = q;
return performSearch(q, filters);
}
return { query, results, isSearching, error, search } as const;
}
VueUse: La Librería Esencial de Composables
VueUse es una colección de 200+ composables que cubren APIs del navegador, sensores, animaciones, gestión de estado y utilidades. En lugar de escribir tu propio useLocalStorage, useMediaQuery, useIntersectionObserver o useFetch, VueUse provee implementaciones probadas y nativas de TypeScript. Es la librería utilitaria estándar de facto para proyectos Vue 3.useLocalStorage, useMediaQuery, useIntersectionObserver, or useFetch, VueUse provides tested, TypeScript-native implementations. It is the de facto standard utility library for Vue 3 projects.
2. Gestión de Estado: De Vuex a Pinia
Vuex: El Estándar Legado
Vuex era la librería oficial de gestión de estado de Vue 2. Impone un patrón estricto: state (los datos), getters (estado derivado computado), mutations (cambios de estado síncronos) y actions (operaciones async que hacen commit de mutations). Aunque efectivo, Vuex tenía puntos débiles: sin soporte TypeScript, boilerplate verboso, y la distinción mutation/action que agregaba complejidad sin beneficio proporcional.
Pinia: El Reemplazo Moderno
Pinia es el reemplazo oficial de Vuex en Vue 3. Elimina las mutations por completo (las actions modifican el estado directamente), provee soporte de primera clase de TypeScript con inferencia de tipos completa, soporta múltiples stores sin módulos anidados, y se integra con Vue DevTools. Cada store es una función composable, haciéndola natural de usar junto con la Composition API.
// stores/members.ts - Pinia store with TypeScript
import { defineStore } from 'pinia';
import { memberApi } from '@/api/members';
import type { Member, MemberFilters, CreateMemberDto } from '@/types';
interface MembersState {
members: Member[];
selectedId: string | null;
filters: MemberFilters;
loading: boolean;
error: string | null;
}
export const useMembersStore = defineStore('members', {
state: (): MembersState => ({
members: [],
selectedId: null,
filters: { status: 'all', plan: null, sortBy: 'name' },
loading: false,
error: null,
}),
getters: {
activeMembers: (state) => state.members.filter(m => m.status === 'active'),
selectedMember: (state) => state.members.find(m => m.id === state.selectedId) ?? null,
membersByPlan: (state) => {
const grouped = new Map<string, Member[]>();
for (const m of state.members) {
const plan = m.planName ?? 'No Plan';
if (!grouped.has(plan)) grouped.set(plan, []);
grouped.get(plan)!.push(m);
}
return grouped;
},
},
actions: {
async fetchMembers() {
this.loading = true;
this.error = null;
try {
this.members = await memberApi.list(this.filters);
} catch (e) {
this.error = (e as Error).message;
} finally {
this.loading = false;
}
},
async createMember(dto: CreateMemberDto) {
const member = await memberApi.create(dto);
this.members.push(member);
return member;
},
async deleteMember(id: string) {
await memberApi.delete(id);
this.members = this.members.filter(m => m.id !== id);
if (this.selectedId === id) this.selectedId = null;
},
},
});
Setup Stores: Sintaxis Composition API
Pinia también soporta una sintaxis de "setup store" que refleja la Composition API. En lugar de opciones (state, getters, actions), usas ref, computed y funciones regulares. Este enfoque provee máxima flexibilidad y es preferido cuando los stores contienen lógica compleja con watchers y composables.ref, computed, and regular functions. This approach provides maximum flexibility and is preferred when stores contain complex logic with watchers and composables.
// Setup store syntax: full Composition API power
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null);
const token = ref<string | null>(localStorage.getItem('token'));
const isAuthenticated = computed(() => !!token.value && !!user.value);
const isAdmin = computed(() => user.value?.role === 'admin');
async function login(credentials: LoginDto) {
const response = await authApi.login(credentials);
token.value = response.token;
user.value = response.user;
localStorage.setItem('token', response.token);
}
function logout() {
token.value = null;
user.value = null;
localStorage.removeItem('token');
}
// Auto-fetch user profile when token exists
watch(token, async (newToken) => {
if (newToken && !user.value) {
try { user.value = await authApi.getProfile(); }
catch { logout(); }
}
}, { immediate: true });
return { user, token, isAuthenticated, isAdmin, login, logout };
});
3. Nuxt 3: SSR, SSG e ISR
Nuxt 3 es el meta-framework para Vue.js, análogo a Next.js para React. Provee enrutamiento basado en archivos, renderizado del lado del servidor (SSR), generación de sitios estáticos (SSG), regeneración estática incremental (ISR), auto-imports y un ecosistema de módulos. Construido sobre Vue 3 y Nitro (un motor de servidor universal), soporta despliegue en Node.js, funciones serverless, edge workers y hosting estático.
Modos de Renderizado: SSR, SSG e ISR
Nuxt soporta múltiples estrategias de renderizado: SSR Universal renderiza en el servidor e hidrata en el cliente en cada request. SSG (Generación de Sitios Estáticos) pre-renderiza todas las páginas en build como HTML estático. ISR (Regeneración Estática Incremental) sirve páginas estáticas cacheadas pero las revalida en segundo plano después de un tiempo configurable (usando swr en routeRules). Renderizado híbrido mezcla estas estrategias por ruta. Para la app white-label de la plataforma, usamos ISR para páginas de listado de gimnasios (revalidadas cada hora) y SSR completo para dashboards de miembros que requerían datos frescos.Universal SSR renders on the server and hydrates on the client for every request. SSG (Static Site Generation) pre-renders all pages at build time into static HTML. ISR (Incremental Static Regeneration) serves cached static pages but revalidates them in the background after a configurable time window (using swr in routeRules). Hybrid rendering mixes these strategies per route. For the white-label app, we used ISR for gym listing pages (revalidated hourly) and full SSR for member dashboards requiring fresh data.
// nuxt.config.ts - Nuxt 3 configuration for white-label app
export default defineNuxtConfig({
ssr: true,
routeRules: {
'/': { prerender: true }, // Static: homepage
'/gyms': { swr: 3600 }, // ISR: revalidate hourly
'/gyms/**': { swr: 300 }, // ISR: revalidate every 5 min
'/dashboard/**': { ssr: true }, // SSR: always fresh
'/api/**': { cors: true, cache: false }, // API: no cache
},
modules: [
'@pinia/nuxt',
'@nuxtjs/i18n',
'@vueuse/nuxt',
'@nuxt/image',
],
i18n: {
locales: [
{ code: 'en', file: 'en.json' },
{ code: 'es', file: 'es.json' },
],
defaultLocale: 'es',
strategy: 'prefix_except_default',
},
runtimeConfig: {
apiSecret: process.env.API_SECRET,
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'https://api.example.com',
tenantId: process.env.NUXT_PUBLIC_TENANT_ID,
},
},
});
Data Fetching: useFetch y useAsyncData
Nuxt provee los composables useFetch y useAsyncData para data fetching que funcionan perfectamente en SSR. Los datos obtenidos en el servidor se serializan y transfieren al cliente, evitando requests duplicados durante la hidratación. Los composables retornan referencias reactivas para los datos, estado pendiente y error, más una función refresh para refetching manual.useFetch and useAsyncData composables for data fetching that work seamlessly in SSR. Data fetched on the server is serialized and transferred to the client, avoiding duplicate requests during hydration. The composables return reactive references for the data, pending state, and error, plus a refresh function for manual refetching.
// pages/gyms/[slug].vue - Dynamic page with SSR data fetching
<script setup lang="ts">
const route = useRoute();
const { locale } = useI18n();
// Data fetched on server, transferred to client on hydration
const { data: gym, error } = await useFetch<Gym>(
() => `/api/gyms/${route.params.slug}`,
{
query: { lang: locale.value },
transform: (data) => ({
...data,
scheduleFormatted: formatSchedule(data.schedule, locale.value),
}),
}
);
// Client-only data: fetched after hydration
const { data: reviews } = useLazyFetch<Review[]>(
() => `/api/gyms/${route.params.slug}/reviews`,
{ server: false } // Skip SSR, fetch on client only
);
// SEO meta tags from fetched data
useHead({
title: () => gym.value?.name ?? 'Gym',
meta: [
{ name: 'description', content: () => gym.value?.description ?? '' },
{ property: 'og:image', content: () => gym.value?.coverImage ?? '' },
],
});
if (error.value) {
throw createError({ statusCode: 404, message: 'Gym not found' });
}
</script>
Arquitectura White-Label
Una aplicación white-label sirve múltiples marcas desde una sola base de código. En Nuxt, esto se logra a través de configuración en runtime: el ID del tenant determina el tema (colores, logo, fuentes), contenido y endpoints de API. El middleware resuelve el tenant desde el hostname, y un store de Pinia mantiene la configuración del tenant que maneja toda la UI.
4. Vue Router y Navigation Guards
Vue Router provee enrutamiento del lado del cliente con modo history (URLs limpias), modo hash (fallback) y modo memory (SSR). En Nuxt, el enrutamiento es basado en archivos: la estructura del directorio pages/ define las rutas automáticamente. Los segmentos dinámicos usan sintaxis de corchetes ([id].vue), las rutas catch-all usan sintaxis de spread ([...slug].vue).pages/ directory structure defines routes automatically. Dynamic segments use bracket syntax ([id].vue), catch-all routes use spread syntax ([...slug].vue).
Navigation Guards
Los navigation guards interceptan transiciones de ruta para autenticación, autorización y prefetching de datos. Los guards globales (beforeEach) se ejecutan en cada navegación. Los guards a nivel de ruta (beforeEnter) se ejecutan en rutas específicas. Los guards de componente (onBeforeRouteLeave) manejan prompts de cambios sin guardar. En Nuxt, usa archivos de middleware de ruta para organización limpia de guards.beforeEach) run on every navigation. Route-level guards (beforeEnter) run on specific routes. Component guards (onBeforeRouteLeave) handle unsaved changes prompts. In Nuxt, use route middleware files for clean guard organization.
// middleware/auth.ts - Nuxt route middleware
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore();
if (!authStore.isAuthenticated) {
return navigateTo({
path: '/login',
query: { redirect: to.fullPath },
});
}
// Role-based access control
const requiredRole = to.meta.role as string | undefined;
if (requiredRole && authStore.user?.role !== requiredRole) {
return navigateTo('/unauthorized');
}
});
// middleware/tenant.global.ts - Global middleware: resolve tenant on every request
export default defineNuxtRouteMiddleware(async (to) => {
const tenantStore = useTenantStore();
if (tenantStore.isLoaded) return;
const hostname = useRequestHeaders()['host'] || window.location.hostname;
try {
await tenantStore.resolveFromHostname(hostname);
} catch {
throw createError({ statusCode: 404, message: 'Gym not found' });
}
});
Enrutamiento Basado en Archivos en Nuxt
pages/
index.vue # /
login.vue # /login
gyms/
index.vue # /gyms
[slug].vue # /gyms/:slug (dynamic)
[slug]/
schedule.vue # /gyms/:slug/schedule
plans.vue # /gyms/:slug/plans
dashboard/
index.vue # /dashboard (requires auth middleware)
profile.vue # /dashboard/profile
subscriptions/
index.vue # /dashboard/subscriptions
[id].vue # /dashboard/subscriptions/:id
[...slug].vue # Catch-all: 404 page
5. Patrones de Diseño de Componentes
Slots: Composición Flexible de Componentes
Los slots son el mecanismo de Vue para composición de componentes, análogo al children prop de React pero más poderoso. Los named slots permiten múltiples áreas de contenido. Los scoped slots exponen datos del hijo al contenido del slot del padre. La directiva v-slot provee una API limpia para ambos patrones.v-slot directive provides a clean API for both patterns.
<!-- DataTable.vue: generic table with scoped slots -->
<template>
<div class="data-table">
<div class="toolbar">
<slot name="toolbar" :selectedCount="selectedIds.size">
<span>{{ total }} items</span>
</slot>
</div>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key" @click="toggleSort(col.key)">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in sortedData" :key="row.id">
<!-- Scoped slot: parent decides how to render each cell -->
<td v-for="col in columns" :key="col.key">
<slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
<slot name="pagination" :page="page" :totalPages="totalPages" :goTo="goToPage" />
</div>
</template>
<!-- Usage: customizing cells and toolbar -->
<DataTable :data="members" :columns="memberColumns">
<template #toolbar="{ selectedCount }">
<button v-if="selectedCount" @click="bulkDelete">Delete {{ selectedCount }}</button>
</template>
<template #cell-status="{ value }">
<StatusBadge :status="value" />
</template>
<template #cell-actions="{ row }">
<button @click="editMember(row.id)">Edit</button>
</template>
</DataTable>
Provide/Inject: Inyección de Dependencias
El sistema provide/inject de Vue permite pasar datos hacia abajo en el árbol de componentes sin prop drilling, similar a React Context. Un componente padre provee valores, y cualquier descendiente puede inyectarlos. Con la Composition API, provide e inject aceptan símbolos InjectionKey para seguridad de tipos.provide/inject system enables passing data down the component tree without prop drilling, similar to React Context. A parent component provides values, and any descendant can inject them. With the Composition API, provide and inject accept InjectionKey symbols for type safety.
// Typed provide/inject with InjectionKey
import { provide, inject, type InjectionKey } from 'vue';
interface ThemeConfig {
primaryColor: string;
fontFamily: string;
borderRadius: string;
}
const ThemeKey: InjectionKey<ThemeConfig> = Symbol('ThemeConfig');
// In a parent component or app setup
provide(ThemeKey, {
primaryColor: tenantStore.theme.primary,
fontFamily: tenantStore.theme.font,
borderRadius: '8px',
});
// In any descendant component
const theme = inject(ThemeKey);
// theme is typed as ThemeConfig | undefined
Componentes Renderless
Los componentes renderless contienen lógica pero no template. Exponen su estado y métodos a través de scoped slots, dejando que el padre controle el renderizado completamente. Este patrón es poderoso para comportamientos reutilizables como validación de formularios, drag-and-drop, scroll infinito y wrappers de data fetching.
Teleport: Renderizado Fuera del Árbol de Componentes
El componente
<!-- ConfirmDialog.vue: modal teleported to body -->
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal-content" role="dialog" aria-modal="true">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<div class="actions">
<button @click="close">Cancel</button>
<button @click="confirm" class="primary">Confirm</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
defineProps<{ title: string; message: string; visible: boolean }>();
const emit = defineEmits<{
(e: 'confirm'): void;
(e: 'close'): void;
}>();
function confirm() { emit('confirm'); }
function close() { emit('close'); }
</script>
overflow:hidden or z-index stacking context. By teleporting to body, the modal renders at the top of the DOM while its reactive state stays bound to the parent component. Teleport also supports conditional targets with the disabled prop for SSR compatibility.6. Transiciones y Animaciones
Vue provee los componentes built-in
Transiciones de Elemento Único
<!-- Fade transition using CSS classes -->
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade" mode="out-in">
<div v-if="show" class="panel">Content here</div>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
Transiciones de Lista con TransitionGroup
<!-- Animated member list with enter/leave/move transitions -->
<template>
<TransitionGroup name="list" tag="ul" class="member-list">
<li v-for="member in sortedMembers" :key="member.id">
{{ member.name }} — {{ member.planName }}
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active, .list-leave-active {
transition: all 0.4s ease;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* FLIP animation for reordering */
.list-move {
transition: transform 0.4s ease;
}
/* Ensure leaving items are taken out of flow */
.list-leave-active {
position: absolute;
}
</style>
Hooks de JavaScript para Animaciones Complejas
Para animaciones que requieren lógica JavaScript (por ejemplo, entradas escalonadas, movimiento basado en física o integración con GSAP), usa los hooks @before-enter, @enter, @leave. Configura :css="false" para omitir la detección de clases CSS y depender completamente de los hooks de JavaScript.@before-enter, @enter, @leave hooks. Set :css="false" to skip CSS class detection and rely entirely on the JavaScript hooks.
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
:css="false"
>
<div v-if="show">Animated content</div>
</Transition>
<script setup lang="ts">
import gsap from 'gsap';
function onBeforeEnter(el: Element) {
(el as HTMLElement).style.opacity = '0';
(el as HTMLElement).style.transform = 'translateY(20px)';
}
function onEnter(el: Element, done: () => void) {
gsap.to(el, { opacity: 1, y: 0, duration: 0.5, onComplete: done });
}
function onLeave(el: Element, done: () => void) {
gsap.to(el, { opacity: 0, y: -20, duration: 0.3, onComplete: done });
}
</script>
nuxt.config.ts with app: { pageTransition: { name: 'page', mode: 'out-in' } }, or per-page using definePageMeta({ pageTransition: { name: 'slide' } }). This provides smooth navigation animations across the entire application with minimal configuration.7. Testing con Vitest
Vitest es un framework de testing nativo de Vite que provee ejecución instantánea de tests, soporte nativo de ESM y una API compatible con Jest. Comparte la configuración de Vite (aliases, plugins, transforms), eliminando la duplicación de config que plagaba los setups de Jest con Vue. Combinado con Vue Test Utils, permite testear componentes, composables y stores con boilerplate mínimo.
Testeando Componentes
// MemberCard.spec.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import MemberCard from '@/components/MemberCard.vue';
import type { Member } from '@/types';
const mockMember: Member = {
id: '1', name: 'Ana Garcia', email: '[email protected]',
status: 'active', planName: 'Premium', joinedAt: '2024-01-15',
};
describe('MemberCard', () => {
it('displays member information', () => {
const wrapper = mount(MemberCard, {
props: { member: mockMember },
global: { plugins: [createTestingPinia()] },
});
expect(wrapper.text()).toContain('Ana Garcia');
expect(wrapper.text()).toContain('Premium');
expect(wrapper.find('[data-testid="status-badge"]').text()).toBe('Active');
});
it('emits edit event when edit button is clicked', async () => {
const wrapper = mount(MemberCard, {
props: { member: mockMember },
global: { plugins: [createTestingPinia()] },
});
await wrapper.find('[data-testid="edit-btn"]').trigger('click');
expect(wrapper.emitted('edit')).toHaveLength(1);
expect(wrapper.emitted('edit')![0]).toEqual([mockMember.id]);
});
});
Testeando Composables
// composables/useMemberSearch.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { flushPromises } from '@vue/test-utils';
import { useMemberSearch } from '@/composables/useMemberSearch';
import { memberApi } from '@/api/members';
vi.mock('@/api/members');
describe('useMemberSearch', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('searches members with debounce', async () => {
const mockResults = [{ id: '1', name: 'Ana' }];
vi.mocked(memberApi.search).mockResolvedValue(mockResults);
// Must be called inside a Vue component context
const { query, results, isSearching, search } = withSetup(() =>
useMemberSearch(100) // 100ms debounce for tests
);
search('Ana');
expect(isSearching.value).toBe(false); // Not yet (debounced)
await vi.advanceTimersByTimeAsync(100);
await flushPromises();
expect(memberApi.search).toHaveBeenCalledWith('Ana', undefined);
expect(results.value).toEqual(mockResults);
expect(isSearching.value).toBe(false);
});
});
Testeando Stores de Pinia
Pinia provee @pinia/testing para testear stores en aislamiento. createTestingPinia crea una instancia fresca de Pinia con estado inicial opcional y actions auto-stubbed. Puedes testear getters del store con diferentes configuraciones de estado, verificar efectos secundarios de actions, y mockear llamadas API sin tocar backends reales.@pinia/testing for testing stores in isolation. createTestingPinia creates a fresh Pinia instance with optional initial state and auto-stubbed actions. You can test store getters with different state configurations, verify action side effects, and mock API calls without touching real backends.
// stores/members.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useMembersStore } from '@/stores/members';
import { memberApi } from '@/api/members';
vi.mock('@/api/members');
describe('useMembersStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('fetches members and updates state', async () => {
const mockMembers = [{ id: '1', name: 'Ana', status: 'active' }];
vi.mocked(memberApi.list).mockResolvedValue(mockMembers);
const store = useMembersStore();
await store.fetchMembers();
expect(store.members).toEqual(mockMembers);
expect(store.loading).toBe(false);
expect(store.error).toBeNull();
});
it('computes active members correctly', () => {
const store = useMembersStore();
store.$patch({
members: [
{ id: '1', status: 'active' },
{ id: '2', status: 'expired' },
{ id: '3', status: 'active' },
],
});
expect(store.activeMembers).toHaveLength(2);
});
});
8. Vue DevTools
Vue DevTools es una extensión de navegador (Chrome, Firefox, Edge) y una app Electron independiente para depurar aplicaciones Vue. Provee un inspector del árbol de componentes, visor de estado reactivo, depurador de stores Pinia, timeline de Vue Router, perfilador de rendimiento y rastreador de eventos. En Nuxt 3, un panel integrado de DevTools (Nuxt DevTools) agrega inspección de rutas del servidor, gestión de módulos y análisis de auto-imports.
Características Principales
Component Inspector: Select any component in the tree to view its props, reactive state (refs, reactive objects), computed values, emitted events, and slot content in real time. Edit reactive values directly in the panel to test different states without changing code.
Pinia Integration: View all active stores, their current state, and a timeline of every action dispatched. Time-travel debugging lets you step backward and forward through state mutations, making it easy to trace bugs to the specific action that caused them.
Router and Timeline: The timeline tab shows every navigation event, lifecycle hook, and performance metric. Filter by component or event type to isolate specific behaviors. The router panel displays all registered routes, their params, and middleware chain.
Nuxt DevTools
Nuxt 3 incluye su propia capa de DevTools que se abre como un panel in-app (Shift+Alt+D). Muestra todos los composables auto-importados, módulos activos, rutas API del servidor, configuración de runtime y el grafo de componentes de la aplicación. La integración con VS Code te permite hacer clic en un componente del panel y saltar directamente a su archivo fuente.Shift+Alt+D). It shows all auto-imported composables, active modules, server API routes, runtime config, and the application's component graph. The built-in VS Code integration lets you click a component in the panel and jump directly to its source file.
// nuxt.config.ts - Enable Nuxt DevTools
export default defineNuxtConfig({
devtools: {
enabled: true, // Enable in development
timeline: {
enabled: true, // Track component events in timeline
},
},
});
9. Vite: La Herramienta de Build
Vite es la herramienta de build por defecto para Vue 3 y Nuxt 3. Usa módulos ES nativos durante el desarrollo para inicio instantáneo del servidor (sin bundling), y Rollup para builds de producción optimizados. Hot Module Replacement (HMR) refleja cambios en menos de 50ms sin importar el tamaño de la aplicación, una mejora dramática sobre el enfoque de rebuild completo de Webpack.
Configuración
// vite.config.ts - Vue 3 project configuration
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
target: 'es2020',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-ui': ['@headlessui/vue', '@heroicons/vue'],
},
},
},
},
});
Ventajas Principales
Instant Dev Server: Vite serves source files as native ES modules. The browser requests each module individually, and Vite transforms them on-demand. Only the modules you actually import are processed, making startup time nearly constant regardless of application size.
Builds de Producción Optimizados: Vite usa Rollup internamente para producción, aplicando tree-shaking, code-splitting, extracción de CSS y hashing de assets. La opción manualChunks te permite controlar la división de bundles de vendors para caché óptimo. Los imports dinámicos (() => import('.
Plugin Ecosystem: Vite's plugin API is compatible with Rollup plugins, giving access to a vast ecosystem. Key plugins for Vue projects include @vitejs/plugin-vue (SFC support), unplugin-auto-import (auto-import Vue APIs), unplugin-vue-components (auto-register components), and vite-plugin-pwa (Progressive Web App support).
vite.config.ts. Use vite key in nuxt.config.ts to customize Vite-specific options like plugins, CSS preprocessor settings, and optimization overrides.10. Vue 3.5, Vapor Mode y Nuxt 4
Vue 3.5: Mejoras de Rendimiento y DX
Vue 3.5 (estable) introduce Reactive Props Destructuring: los props pueden desestructurarse directamente en <script setup> manteniendo la reactividad, eliminando la necesidad de toRefs(). Los nuevos composables incluyen useTemplateRef() para refs de template con seguridad de tipos y useId() para IDs únicos compatibles con SSR. El componente
<script setup lang="ts">
// Vue 3.5: Reactive Props Destructuring (no toRefs needed)
const { name, status = 'active' } = defineProps<{
name: string;
status?: string;
}>();
// name and status are reactive — they track in computed/watch
const displayName = computed(() => `${name} (${status})`);
// Vue 3.5: useTemplateRef for type-safe refs
const inputRef = useTemplateRef<HTMLInputElement>('searchInput');
// Vue 3.5: useId for SSR-safe unique IDs
const id = useId(); // e.g., "v-0", "v-1" — consistent across server and client
</script>
<template>
<label :for="id">Search</label>
<input :id="id" ref="searchInput" />
<p>{{ displayName }}</p>
<!-- Vue 3.5: Deferred Teleport — waits for target to exist -->
<Teleport to="#modal-container" defer>
<Modal v-if="showModal" />
</Teleport>
</template>
Vue 3.6 / Vapor Mode (Beta Dic 2025)
Vapor Mode (beta diciembre 2025) es el avance de rendimiento más significativo de Vue. Compila templates directamente a operaciones DOM, omitiendo el Virtual DOM por completo. Los benchmarks de rendimiento igualan a Solid.js. Vapor Mode se activa por componente, por lo que los componentes VDOM y Vapor pueden coexistir en el mismo árbol. El motor de reactividad Alien Signals, adoptado en Vue 3.6, es sustancialmente más rápido que el sistema anterior basado en Proxies.bypassing the Virtual DOM entirely. Performance benchmarks match Solid.js. Vapor Mode is opt-in at the component level, so VDOM and Vapor components can coexist in the same tree. The Alien Signals reactivity engine, adopted in Vue 3.6, is substantially faster than the previous Proxy-based system.
// Vue 3.6 Vapor Mode: opt-in per component
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
vapor: true, // Enable Vapor compilation for opted-in components
}),
],
});
// MemberCard.vapor.vue — compiled to direct DOM operations
<script setup lang="ts">
const { name, status } = defineProps<{ name: string; status: string }>();
const isActive = computed(() => status === 'active');
</script>
<template>
<!-- No Virtual DOM diffing — direct DOM updates when signals change -->
<div :class="{ active: isActive }">
<h3>{{ name }}</h3>
<span>{{ status }}</span>
</div>
</template>
Nuxt 4 (Estable Julio 2025)
Nuxt 4 (estable julio 2025) introduce una nueva estructura de directorio app/ que separa el código de la aplicación de la configuración. El data fetching compartido evita requests redundantes al servidor entre layouts y páginas. El soporte de TypeScript mejora con verificación de tipos más estricta y mejor autocompletado. El CLI es significativamente más rápido, y la migración desde Nuxt 3 es directa con una capa de compatibilidad y codemods automatizados.app/ directory structure that separates application code from configuration. Shared data fetching avoids redundant server requests across layouts and pages. TypeScript support is improved with stricter type checking and better auto-completion. The CLI is significantly faster, and migration from Nuxt 3 is straightforward with a compatibility layer and automated codemods.
# Nuxt 4: New directory structure
my-app/
app/ # Application code (new in Nuxt 4)
components/
composables/
layouts/
middleware/
pages/
plugins/
app.vue
server/ # Server routes and middleware
shared/ # Shared types and utils (app + server)
nuxt.config.ts
package.json
# Migration from Nuxt 3
npx nuxi upgrade --force # Update to Nuxt 4
npx nuxi migrate # Run automated codemods
Últimas Actualizaciones (Junio 2026)
Vue 3.5: useTemplateRef, useId y Optimizaciones de Reactividad
Vue 3.5 (en mantenimiento en v3.5.38) introdujo useTemplateRef(), que obtiene refs de template vía IDs de string en runtime en lugar de coincidencia de nombres de variables en tiempo de compilación. Esto habilita bindings de ref dinámicos a IDs cambiantes y funciona directamente dentro de composables, retornando un ShallowRef de solo lectura tipado. El nuevo useId() genera IDs únicos para atributos de accesibilidad (similar al useId de React). Vue 3.5 también entregó optimizaciones importantes de reactividad: el uso de memoria para objetos reactivos cayó un 56%, y el sistema de reactividad fue reestructurado para reducir re-computaciones en estructuras reactivas profundamente anidadas.useTemplateRef(), which obtains template refs via runtime string IDs instead of compile-time variable name matching. This enables dynamic ref bindings to changing IDs and works directly inside composables, returning a typed read-only ShallowRef. The new useId() generates unique IDs for accessibility attributes (similar to React's useId). Vue 3.5 also delivered major reactivity optimizations: memory usage for reactive objects dropped 56%, and the reactivity system was restructured to reduce re-computations for deeply nested reactive structures.
Vue 3.6 Vapor Mode: Beta con Paridad de Características Completa
Vue 3.6.0-beta.1 se lanzó a finales de 2025 con Vapor Mode alcanzando paridad de características completa con todas las funcionalidades estables del modo Virtual DOM (excluyendo Suspense); la beta sigue iterando y ya va por 3.6.0-beta.15 (junio 2026). Vapor Mode compila templates directamente a operaciones DOM, omitiendo el Virtual DOM por completo e igualando el rendimiento de Solid.js. El motor de reactividad Alien Signals que impulsa Vue 3.6 es sustancialmente más rápido que el sistema anterior basado en Proxies. Vapor Mode se activa por componente (archivos .vapor.vue), por lo que componentes VDOM y Vapor coexisten sin problemas. Se proyecta un release estable para Q4 2026, con soporte de Transition y KeepAlive esperado en Q3..vapor.vue files), so VDOM and Vapor components coexist seamlessly. A stable release is projected for Q4 2026, with Transition and KeepAlive support expected in Q3.
Nuxt 4.4 y Madurez del Ecosistema
Nuxt 4 (estable desde julio 2025) ha alcanzado v4.4.8, trayendo una estructura de directorio app/ más limpia, data fetching built-in más inteligente, soporte mejorado de TypeScript y un CLI más rápido. El ecosistema Nuxt en 2026 es estable y maduro, con Nitro 3 (el motor de servidor) siendo finalizado para Nuxt 5. Vue 3.5 permanece en mantenimiento (v3.5.38), proveyendo una base sólida de producción mientras Vapor Mode madura en la beta 3.6.app/ directory structure, smarter built-in data fetching, improved TypeScript support, and a faster CLI. The Nuxt ecosystem in 2026 is stable and mature, with Nitro 3 (the server engine) being finalized for Nuxt 5. Vue 3.5 remains in maintenance (v3.5.38), providing a solid production foundation while Vapor Mode matures in the 3.6 beta.