DevTools

Cheatsheet Vue.js

Framework frontend progressivo para interfaces reativas

Volver a los lenguajes
Vue.js
93 tarjetas encontradas
Categorías:
Versiones:

Setup e Estrutura


10 cards
Crear proyecto (Vite)
# Crear con create-vue (oficial):
npm create vue@latest mi-proyecto
cd mi-proyecto
npm install
npm run dev

# Opciones durante el setup:
# - TypeScript? Sí/No
# - JSX? Sí/No
# - Vue Router? Sí/No
# - Pinia? Sí/No
# - Vitest? Sí/No
# - ESLint + Prettier? Sí/No

# Alternativa manual:
npm create vite@latest app -- --template vue

create-vue es el scaffold oficial (basado en Vite). Configura TypeScript, Router, Pinia y tests según se elija. npm run dev inicia un servidor con HMR (Hot Module Replacement). Estructura generada: src/, public/, vite.config.js. Vite es mucho más rápido que Webpack en desarrollo.

Estructura de carpetas
src/
├── assets/          # imágenes, fuentes
├── components/      # componentes reutilizables
│   ├── AppHeader.vue
│   └── AppFooter.vue
├── composables/     # lógica compartida (use*)
│   └── useCounter.js
├── views/           # páginas (rutas)
│   ├── HomeView.vue
│   └── AboutView.vue
├── router/          # configuración de rutas
│   └── index.js
├── stores/          # Pinia stores
│   └── counter.js
├── App.vue          # componente raíz
└── main.js          # entry point

Estructura estándar de un proyecto Vue. components/ para UI reutilizable. views/ (o pages/) para componentes de ruta. composables/ para lógica compartida (prefijo use). stores/ para estado global (Pinia). router/ para navegación. Convención: PascalCase para archivos .vue.

Directivas built-in (resumen)
{{ text }}            <!-- interpolación -->
v-bind:src="url"      <!-- :src atajo -->
v-on:click="fn"       <!-- @click atajo -->
v-model="value"       <!-- two-way binding -->
v-if="cond"           <!-- condicional -->
v-else-if="other"
v-else
v-show="visible"      <!-- display toggle -->
v-for="item in list"  <!-- iteración -->
v-html="markup"       <!-- HTML raw -->
v-text="text"         <!-- textContent -->
v-slot:name           <!-- #name atajo -->
v-pre                 <!-- no compilar -->
v-once                <!-- renderizar 1x -->
v-memo="[dep]"        <!-- memoizar -->

Vue tiene directivas built-in con prefijo v-. Atajos: : para v-bind, @ para v-on, # para v-slot. v-if elimina/crea DOM; v-show usa display:none. v-model para two-way binding. v-once y v-memo para optimización.

Single File Component (SFC)
<!-- MiComponente.vue -->
<template>
  <div class="box">
    <h1>{{ title }}</h1>
  </div>
</template>

<script setup>
import { ref } from "vue";
const title = ref("Hola Vue");
</script>

<style scoped>
.box {
  padding: 1rem;
  border: 1px solid #ccc;
}
</style>

El SFC combina template, script y style en un archivo .vue. <script setup> es la sintaxis moderna (Composition API). <style scoped> limita el CSS al componente (no afecta a otros). Cada SFC es un componente reutilizable. El compilador Vue transforma los SFC en render functions de JavaScript.

Vía CDN (sin build)
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <button @click="counter++">
      Clics: {{ counter }}
    </button>
  </div>

  <script>
    const { creupTopp, ref } = Vue;
    creupTopp({
      setup() {
        const message = ref("¡Hola!");
        const counter = ref(0);
        return { message, counter };
      }
    }).mount("#app");
  </script>
</body>
</html>

Uso rápido sin build: incluir Vue vía CDN y usar Vue.creupTopp(). Sin SFC, sin importaciones — todo en el HTML. setup() retorna un objeto con datos para el template. Ideal para prototipos y aprendizaje. Para producción, usar Vite con build (tree-shaking, optimizaciones). vue.global.js expone todo en el objeto Vue.

Directivas personalizadas
<script setup>
// Directiva local (prefijo v- automático):
const vFocus = {
  mounted(el) {
    el.focus();
  }
};

// Con más hooks:
const vColor = {
  mounted(el, binding) {
    el.style.color = binding.value;
  },
  updated(el, binding) {
    el.style.color = binding.value;
  }
};
</script>

<template>
  <input v-focus />
  <p v-color="'red'">Texto rojo</p>
  <p v-color="dynamicColor">Color dinámico</p>
</template>

Directivas personalizadas para manipulación directa del DOM. En script setup, la variable vNombre se registra como v-nombre. Hooks: mounted, updated, unmounted. binding.value es el valor pasado. Usar cuando ref + lifecycle no bastan. Registrar globalmente con app.directive().

creupTopp y mount
// main.js — punto de entrada:
import { creupTopp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";

const app = creupTopp(App);

app.use(router);
app.use(createPinia());

// Plugin/directiva global:
app.directive("focus", {
  mounted(el) { el.focus(); }
});

app.mount("#app");

creupTopp() crea la instancia raíz. app.use() registra plugins (Router, Pinia). app.directive() registra directivas globales. app.mount("#app") monta en el elemento HTML. Todo se registra antes de mount(). El App.vue es el componente raíz que contiene <router-view>.

vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(
        new URL("./src", import.meta.url)
      ),
    },
  },
  server: {
    port: 3000,
    proxy: {
      "/api": "http://localhost:8000",
    },
  },
});

vite.config.js configura el build. @vitejs/plugin-vue compila los SFC. El alias "@" mapea a src/ (imports más cortos). server.port define el puerto del dev server. proxy redirige las peticiones API (evita CORS en dev). Vite detecta cambios y recarga automáticamente (HMR).

script setup vs Options API
<!-- Composition API (recomendado): -->
<script setup>
import { ref, computed } from "vue";
const n = ref(0);
const double = computed(() => n.value * 2);
function inc() { n.value++; }
</script>

<!-- Options API (clásico): -->
<script>
export default {
  data() { return { n: 0 }; },
  computed: {
    double() { return this.n * 2; }
  },
  methods: {
    inc() { this.n++; }
  }
};
</script>

<script setup> es la sintaxis moderna: menos boilerplate, mejor TypeScript, variables usadas directamente en el template. La Options API organiza por tipo (data, methods, computed). La Composition API permite agrupar lógica relacionada. Ambas funcionan en Vue 3. script setup es el estándar recomendado por el equipo de Vue.

TypeScript en Vue
<script setup lang="ts">
import { ref, computed } from "vue";

// Tipado automático:
const name = ref<string>("Ana");
const age = ref(30); // inferido: number

// Props tipadas:
const props = defineProps<{
  title: string;
  counter?: number;
}>();

// Emits tipados:
const emit = defineEmits<{
  save: [data: string];
  cancel: [];
}>();

// Composable tipado:
const items = ref<string[]>([]);
</script>

lang="ts" activa TypeScript en el SFC. ref<T>() para tipado explícito (o inferencia automática). defineProps<T>() con tipos TypeScript (sin runtime). defineEmits<T>() con tuple labels. Vue 3 + TypeScript tiene soporte de primera clase. IDE: Volar (extensión oficial) para type-checking en templates.

Template e Diretivas


11 cards
Interpolación (Mustache)
<template>
  <!-- Texto: -->
  <p>{{ message }}</p>
  <p>{{ 1 + 1 }}</p>
  <p>{{ active ? "Sí" : "No" }}</p>
  <p>{{ name.toUpperCase() }}</p>

  <!-- NO funciona en atributos: -->
  <!-- <img src="{{ url }}"> ← ERROR -->
  <!-- Usar v-bind: -->
  <img :src="url" />

  <!-- Renderizar 1x (sin reactividad): -->
  <p v-once>{{ staticText }}</p>
</template>

{{ }} (mustache) renderiza texto reactivo. Acepta expresiones JavaScript simples (ternario, métodos). No funciona en atributos HTML — usar v-bind (:attr). Se actualiza automáticamente cuando los datos cambian. v-once renderiza solo una vez (optimización). No usar para HTML — usar v-html.

v-model (two-way binding)
<template>
  <!-- Texto: -->
  <input v-model="name" placeholder="Nombre" />

  <!-- Textarea: -->
  <textarea v-model="description"></textarea>

  <!-- Checkbox: -->
  <input type="checkbox" v-model="accepted" id="terms" />

  <!-- Radio: -->
  <input type="radio" v-model="role" value="admin" />
  <input type="radio" v-model="role" value="user" />

  <!-- Select: -->
  <select v-model="country">
    <option value="">Elegir...</option>
    <option value="pt">Portugal</option>
    <option value="br">Brasil</option>
  </select>

  <!-- Modificadores: -->
  <input v-model.trim="email" />
  <input v-model.number="age" type="number" />
  <input v-model.lazy="search" />
</template>

v-model crea una ligación bidireccional (input → dato → input). Funciona con input, textarea, select, checkbox, radio. Modificadores: .trim elimina espacios, .number convierte a número, .lazy actualiza en change en vez de input. En componentes, equivale a :modelValue + @update:modelValue.

Slots (contenido proyectado)
<!-- Card.vue (componente): -->
<template>
  <div class="card">
    <slot>Contenido por defecto</slot>
  </div>
</template>

<!-- Uso: -->
<Card>
  <p>Mi contenido custom</p>
</Card>

<!-- Sin contenido → muestra "Contenido por defecto" -->
<Card />

<!-- Named slots: -->
<!-- Layout.vue: -->
<template>
  <header><slot name="top"></slot></header>
  <main><slot></slot></main>
  <footer><slot name="bottom"></slot></footer>
</template>

<!-- Uso con # (atajo v-slot): -->
<Layout>
  <template #top><h1>Título</h1></template>
  <p>Contenido principal</p>
  <template #bottom><small>© 2024</small></template>
</Layout>

<slot> define puntos de inserción de contenido. Sin contenido → muestra el fallback. Named slots (name="x") para múltiples áreas. Atajo #nombre = v-slot:nombre. El slot default no necesita template wrapper. Los slots permiten composición flexible sin props. Base de design systems y layouts.

v-bind (binding de atributos)
<!-- Sintaxis completa y atajo: -->
<img v-bind:src="url" />
<img :src="url" />

<!-- Múltiples atributos: -->
<a :href="link" :title="description" :target="external ? '_blank' : '_self'">
  Link
</a>

<!-- Objeto de atributos (spread): -->
<div v-bind="{ id: 'box', class: 'active', 'data-x': 1 }"></div>

<!-- Class dinámica: -->
<div :class="{ active: isActive, error: hasError }"></div>
<div :class="['base', isActive ? 'active' : '']"></div>

<!-- Style dinámico: -->
<div :style="{ color: color, fontSize: size + 'px' }"></div>

v-bind (atajo :) liga atributos a expresiones reactivas. :class acepta objeto (toggle por boolean) o array. :style acepta objeto CSS (camelCase). v-bind="obj" aplica múltiples atributos de una vez. Esencial para cualquier atributo dinámico. Sin :, el valor es un string literal.

v-html y v-text
<template>
  <!-- v-text (seguro, escapa HTML): -->
  <p v-text="message"></p>
  <!-- Equivale a: <p>{{ message }}</p> -->

  <!-- v-html (interpreta HTML): -->
  <div v-html="htmlContent"></div>

  <!-- CUIDADO: ¡XSS si son datos del usuario! -->
  <!-- Nunca: v-html="userInput" -->
  <!-- Sanitizar antes: -->
  <div v-html="sanitized"></div>
</template>

<script setup>
import { ref } from "vue";
import DOMPurify from "dompurify";

const htmlContent = ref("<strong>Negrita</strong> y <em>cursiva</em>");
const sanitized = ref(DOMPurify.sanitize(htmlContent.value));
</script>

v-html renderiza HTML raw (interpreta tags). v-text define el textContent (escapa todo, seguro). {{ }} es equivalente a v-text. v-html es vulnerable a XSS — nunca usar con input del usuario sin sanitizar. DOMPurify es la biblioteca estándar de sanitización.

Scoped slots
<!-- List.vue (componente): -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<!-- Uso (el padre decide cómo renderizar): -->
<List :items="products">
  <template #default="{ item }">
    <strong>{{ item.name }}</strong>
    <span>{{ item.price }}€</span>
  </template>
</List>

<!-- Otro uso (layout diferente): -->
<List :items="users">
  <template #default="{ item }">
    <img :src="item.avatar" />
    {{ item.email }}
  </template>
</List>

Los scoped slots pasan datos del hijo al padre vía atributos en el <slot>. El padre los recibe con #default="{ item }" (destructuring). El componente controla la lógica; el padre controla el renderizado. Patrón "renderless component" — componente sin markup propio. Esencial para tablas, listas y autocompletes personalizables.

v-if / v-else-if / v-else
<template>
  <p v-if="state === 'loading'">Cargando...</p>
  <p v-else-if="state === 'error'">¡Falló!</p>
  <p v-else>Contenido: {{ data }}</p>

  <!-- Con template (no renderiza div extra): -->
  <template v-if="loggedIn">
    <header>Bienvenido</header>
    <nav>Menú</nav>
  </template>

  <!-- v-show (alternativa): -->
  <p v-show="visible">Toggle con display</p>
</template>

<script setup>
import { ref } from "vue";
const state = ref("loading");
const loggedIn = ref(true);
const visible = ref(false);
</script>

v-if crea/destruye elementos en el DOM (renderizado condicional real). v-show solo alterna display:none (el elemento siempre está en el DOM). <template v-if> agrupa sin renderizar un wrapper. Usar v-if para condiciones raras; v-show para toggles frecuentes (menos re-render). Acepta cualquier expresión truthy/falsy.

Class y style dinámicos
<template>
  <!-- Objeto (toggle por boolean): -->
  <div :class="{ active: isActive, 'large-text': large }">
  </div>

  <!-- Array: -->
  <div :class="[baseClass, isActive ? 'active' : '']">
  </div>

  <!-- Combinar con class estática: -->
  <div class="card" :class="{ 'card-active': selected }">
  </div>

  <!-- Style object: -->
  <div :style="{ color: textColor, fontSize: size + 'px' }">
  </div>

  <!-- Múltiples styles: -->
  <div :style="[baseStyle, overrideStyle]"></div>
</template>

<script setup>
import { ref } from "vue";
const isActive = ref(true);
const textColor = ref("#333");
const size = ref(16);
</script>

:class acepta objeto ({clase: boolean}), array o combinación con class estática. Los nombres con guion necesitan comillas. :style usa camelCase para propiedades CSS. Ambos son reactivos — se actualizan cuando los datos cambian. Preferir :class a :style (separación de responsabilidades, caché CSS).

Refs en el template
<template>
  <input ref="nameField" />
  <div ref="box">Contenido</div>
  <button @click="focusInput">Enfocar input</button>

  <!-- Ref en v-for: -->
  <li v-for="item in items" :key="item.id" ref="itemRefs">
    {{ item.name }}
  </li>
</template>

<script setup>
import { ref, onMounted } from "vue";

const nameField = ref(null);
const box = ref(null);

function focusInput() {
  nameField.value.focus();
}

onMounted(() => {
  console.log(box.value.offsetWidth);
});
</script>

ref="nombre" en el template + const nombre = ref(null) en el script crea una referencia directa al elemento DOM. Acceder con nombre.value (elemento nativo). En v-for, el ref es un array de elementos. Usar para focus(), mediciones y APIs nativas. Preferir la reactividad a la manipulación directa siempre que sea posible.

v-for (listas)
<template>
  <!-- Array: -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </li>

  <!-- Objeto: -->
  <div v-for="(value, key, i) in user" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- Range: -->
  <span v-for="n in 10" :key="n">{{ n }}</span>

  <!-- Con filtro (computed): -->
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
</template>

<script setup>
import { computed } from "vue";
const activeItems = computed(
  () => items.value.filter(i => i.active)
);
</script>

v-for itera arrays, objetos y rangos. :key es obligatorio (identificador único para diff eficiente). El segundo parámetro da el índice. Para objetos: (valor, clave, índice). No usar v-if en el mismo elemento que v-for — filtrar con computed. La key nunca debe ser el índice (si la lista cambia).

Renderizado condicional avanzado
<template>
  <!-- Componente dinámico: -->
  <component :is="currentComponent" />

  <!-- Con KeepAlive (caché): -->
  <KeepAlive>
    <component :is="tab" :key="tab" />
  </KeepAlive>

  <!-- Ternario en el template: -->
  <p>{{ loggedIn ? "Bienvenido" : "Inicia sesión" }}</p>

  <!-- Operador && (short-circuit): -->
  <p v-if="errors.length && showErrors">
    {{ errors[0] }}
  </p>
</template>

<script setup>
import { shallowRef } from "vue";
import LoginForm from "./LoginForm.vue";
import RegisterForm from "./RegisterForm.vue";

const currentComponent = shallowRef(LoginForm);
</script>

<component :is> renderiza componentes dinámicamente. Acepta nombre (string) o referencia. shallowRef para componentes (evita reactividad profunda). KeepAlive preserva el estado al cambiar. :key fuerza la recreación cuando es necesario. Ternarios para condiciones simples inline. v-if para bloques complejos.

Eventos e Métodos


10 cards
v-on y @ (handlers)
<template>
  <!-- Sintaxis completa y atajo: -->
  <button v-on:click="save">Guardar</button>
  <button @click="save">Guardar</button>

  <!-- Expresión inline: -->
  <button @click="counter++">+1</button>

  <!-- Con argumento: -->
  <button @click="remove(item.id)">Eliminar</button>

  <!-- Múltiples handlers: -->
  <button @click="validate(), send()">Enviar</button>

  <!-- Acceder al evento nativo: -->
  <button @click="handler($event)">Click</button>

  <!-- Expresión + evento: -->
  <input @input="name = $event.target.value" />
</template>

v-on (atajo @) enlaza eventos DOM a handlers. Acepta nombre de función, expresión inline o ambas. $event da acceso al evento nativo. Múltiples handlers separados por coma. Funciona con cualquier evento DOM: click, input, submit, keydown, etc. En componentes, escucha emits custom.

v-model en componentes
<!-- Padre: -->
<MyInput v-model="name" />
<!-- Equivale a: -->
<MyInput
  :modelValue="name"
  @update:modelValue="name = $event"
/>

<!-- Hijo (MyInput.vue): -->
<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<!-- Múltiples v-model (Vue 3): -->
<UserForm v-model:name="n" v-model:email="e" />

v-model en un componente es azúcar para :modelValue + @update:modelValue. El hijo recibe vía props y emite update:modelValue. Vue 3 permite múltiples v-model con nombre (v-model:name). Sustituye al .sync de Vue 2. Patrón para inputs, selects y toggles personalizados.

Formularios y eventos
<template>
  <form @submit.prevent="send">
    <input v-model="form.name" @blur="validateField('name')" />
    <span v-if="errors.name" class="error">{{ errors.name }}</span>

    <input v-model="form.email" type="email" />
    <select v-model="form.country">
      <option v-for="c in countries" :key="c" :value="c">{{ c }}</option>
    </select>

    <button :disabled="!formValid">Enviar</button>
  </form>
</template>

<script setup>
import { reactive, computed } from "vue";
const form = reactive({ name: "", email: "", country: "" });
const errors = reactive({ name: "", email: "" });

const formValid = computed(() =>
  form.name.length >= 3 && form.email.includes("@")
);

function send() { /* AJAX */ }
</script>

Patrón de formulario Vue: v-model para binding, @submit.prevent para submit sin reload. reactive() para el objeto del formulario. computed para validación derivada. @blur para validación por campo. :disabled reactivo en el botón. Errores en objeto separado. Limpiar errores en el @input del campo.

Modificadores de evento
<template>
  <!-- .stop: stopPropagation -->
  <div @click="parent">
    <button @click.stop="child">No propaga</button>
  </div>

  <!-- .prevent: preventDefault -->
  <form @submit.prevent="send">...</form>

  <!-- .once: solo dispara 1 vez -->
  <button @click.once="start">Iniciar</button>

  <!-- .self: solo si el target es él mismo -->
  <div @click.self="close">Overlay</div>

  <!-- Encadenar: -->
  <a @click.stop.prevent="handler">Link</a>

  <!-- .capture: fase de captura -->
  <div @click.capture="before">...</div>
</template>

Los modificadores son sufijos después de @evento.. .stop = stopPropagation(). .prevent = preventDefault(). .once elimina el handler tras la 1ª ejecución. .self ignora clics en hijos. .capture escucha en la fase de captura (antes de los hijos). Encadenables. Eliminan la necesidad de llamar métodos del evento manualmente.

Eventos nativos en componentes
<!-- Vue 3: los eventos nativos caen en el root (fallthrough) -->
<MyButton @click="handler" class="extra" />
<!-- click y class van al elemento raíz del componente -->

<!-- Desactivar fallthrough: -->
<script setup>
defineOptions({ inheritAttrs: false });
</script>

<!-- Aplicar manualmente: -->
<template>
  <div class="wrapper">
    <button v-bind="$attrs">Texto</button>
  </div>
</template>

<!-- $attrs contiene: class, style, eventos, atributos -->
<script setup>
// Acceder:
import { useAttrs } from "vue";
const attrs = useAttrs();
</script>

En Vue 3, los atributos/eventos no declarados caen en el elemento raíz del componente (fallthrough). inheritAttrs: false lo desactiva. $attrs (o useAttrs()) contiene todos los atributos/eventos no declarados como props. v-bind="$attrs" los aplica a otro elemento. Útil para wrapper components (inputs, botones).

Debounce en eventos
<script setup>
import { ref } from "vue";

const search = ref("");
const results = ref([]);

// Debounce manual:
let timer;
function onType(e) {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    results.value = await doSearch(search.value);
  }, 300);
}

// Con lodash:
// import { debounce } from "lodash-es";
// const onType = debounce(async () => {
//   results.value = await doSearch(search.value);
// }, 300);

// Con watch (alternativa):
import { watchDebounced } from "@vueuse/core";
watchDebounced(search, async (v) => {
  results.value = await doSearch(v);
}, { debounce: 300 });
</script>

<template>
  <input v-model="search" @input="onType" />
</template>

Debounce evita llamadas excesivas en inputs de búsqueda. clearTimeout + setTimeout manual. lodash debounce para una versión robusta. VueUse tiene watchDebounced (combina watch + debounce). 300ms es un buen default. Esencial para autocomplete, búsqueda en tiempo real y validación asíncrona.

Modificadores de tecla
<template>
  <!-- Teclas específicas: -->
  <input @keyup.enter="send" />
  <input @keydown.esc="close" />
  <input @keyup.tab="next" />
  <input @keydown.delete="clear" />

  <!-- Combinaciones: -->
  <input @keydown.ctrl.s="save" />
  <input @keydown.ctrl.shift.p="palette" />
  <input @keyup.alt.enter="newLine" />

  <!-- Global (con directiva o composable): -->
  <!-- useEventListener(document, 'keydown', fn) -->
</template>

<script setup>
function save(e) {
  e.preventDefault();
  saveData();
}
</script>

Modificadores de tecla: .enter, .esc, .tab, .delete, .space, .up, .down, .left, .right. Combinaciones: .ctrl, .shift, .alt, .meta (Cmd). Para teclas custom: @keyup.f2. keydown para atajos (con preventDefault); keyup para input. Para listeners globales, usar un composable.

Event bus (patrón legado)
// Vue 3 eliminó $on/$off/$emit de las instancias
// Alternativa: mitt (biblioteca externa)

// eventBus.js:
import mitt from "mitt";
export const bus = mitt();

// Componente A (emitir):
import { bus } from "./eventBus";
bus.emit("notification", { msg: "Hola" });

// Componente B (escuchar):
import { bus } from "./eventBus";
import { onMounted, onUnmounted } from "vue";

const handler = (data) => console.log(data);
onMounted(() => bus.on("notification", handler));
onUnmounted(() => bus.off("notification", handler));

// PREFERIR: provide/inject o Pinia

Vue 3 eliminó el event bus nativo ($on/$off). Alternativa: la biblioteca mitt (1kB). Siempre limpiar los listeners en onUnmounted (memory leak). Para comunicación entre componentes, preferir: props/emits (padre-hijo), provide/inject (ancestro-descendiente), Pinia (global). Event bus solo para casos muy específicos.

defineEmits (comunicación hijo→padre)
<!-- Child.vue: -->
<script setup>
const emit = defineEmits(["save", "cancel"]);

function onClick() {
  emit("save", { name: "Ana", age: 30 });
}
</script>
<template>
  <button @click="onClick">Guardar</button>
  <button @click="emit('cancel')">Cancelar</button>
</template>

<!-- Parent.vue: -->
<script setup>
function onSave(data) {
  console.log("Recibido:", data);
}
</script>
<template>
  <Child @save="onSave" @cancel="close" />
</template>

defineEmits() declara los eventos que el componente puede emitir. emit("nombre", payload) envía datos al padre. El padre escucha con @nombre="handler" (igual que eventos DOM). Flujo unidireccional: los datos bajan vía props, los eventos suben vía emits. Nombres en camelCase en el script, kebab-case en el template.

useEventListener (composable)
// composables/useEventListener.js:
import { onMounted, onUnmounted } from "vue";

export function useEventListener(target, event, handler) {
  onMounted(() => target.addEventListener(event, handler));
  onUnmounted(() => target.removeEventListener(event, handler));
}

// Uso en el componente:
<script setup>
import { useEventListener } from "./useEventListener";

useEventListener(window, "resize", () => {
  width.value = window.innerWidth;
});

useEventListener(document, "keydown", (e) => {
  if (e.key === "Escape") close();
});
</script>

Composable que registra el listener en mounted y lo elimina en unmounted automáticamente. Evita memory leaks. Acepta cualquier target (window, document, elemento). Patrón de la biblioteca VueUse. Elimina el boilerplate repetitivo de add/removeEventListener. Esencial para eventos globales (resize, scroll, keyboard).

Componentes


11 cards
Definir y usar componente
<!-- MyButton.vue: -->
<script setup>
</script>
<template>
  <button class="btn">Haz clic aquí</button>
</template>

<!-- App.vue (usar): -->
<script setup>
import MyButton from "./components/MyButton.vue";
</script>
<template>
  <MyButton />
  <my-button />  <!-- kebab-case también funciona -->
</template>

<!-- Registro global (main.js): -->
// app.component("MyButton", MyButton);
// No necesita import en cada uso

Cada archivo .vue es un componente. Importarlo en <script setup> para usarlo en el template. Nombres en PascalCase o kebab-case. El registro global con app.component() evita imports (pero pierde tree-shaking). Los componentes son reutilizables y aislados. Convención: prefijo App o Base para genéricos.

Componentes dinámicos
<template>
  <!-- Tabs con componente dinámico: -->
  <nav>
    <button v-for="tab in tabs" :key="tab"
      :class="{ active: currentTab === tab }"
      @click="currentTab = tab">
      {{ tab }}
    </button>
  </nav>

  <KeepAlive>
    <component :is="components[currentTab]" />
  </KeepAlive>
</template>

<script setup>
import { shallowRef } from "vue";
import ProfileTab from "./ProfileTab.vue";
import SettingsTab from "./SettingsTab.vue";

const tabs = ["Profile", "Settings"];
const currentTab = shallowRef("Profile");
const components = { Profile: ProfileTab, Settings: SettingsTab };
</script>

<component :is> renderiza componentes dinámicamente. Acepta referencia o string (si está registrado). KeepAlive preserva el estado al cambiar (no destruye). shallowRef para valores de componente (evita reactividad profunda). Patrón tabs/wizard. Sin KeepAlive, el estado se pierde en cada cambio.

Expose (API pública)
<!-- Child.vue: -->
<script setup>
import { ref } from "vue";

const inputRef = ref(null);
const internal = ref("secreto");

function focus() {
  inputRef.value.focus();
}

function clear() {
  inputRef.value.value = "";
}

// Solo exponer métodos específicos:
defineExpose({ focus, clear });
</script>
<template>
  <input ref="inputRef" />
</template>

<!-- Parent.vue: -->
<script setup>
import { ref } from "vue";
const child = ref(null);
</script>
<template>
  <Child ref="child" />
  <button @click="child.focus()">Enfocar</button>
  <button @click="child.clear()">Limpiar</button>
</template>

defineExpose() controla lo que el padre puede acceder vía ref en el componente. En script setup, todo es privado por defecto. Exponer solo los métodos necesarios (encapsulación). El padre accede con ref de template + child.value.método(). Alternativa a emits para acciones imperativas (focus, scroll, reset).

defineProps (recibir datos)
<script setup>
// Simple:
const props = defineProps(["title", "value"]);

// Con tipos y validación:
const props = defineProps({
  title: { type: String, required: true },
  counter: { type: Number, default: 0 },
  items: { type: Array, default: () => [] },
  size: {
    type: String,
    validator: (v) => ["sm", "md", "lg"].includes(v)
  }
});

// TypeScript (generics):
// const props = defineProps<{
//   title: string;
//   counter?: number;
// }>();
</script>

<template>
  <h2>{{ title }}</h2>
  <p>Cuenta: {{ counter }}</p>
</template>

defineProps() declara los datos que el componente recibe del padre. type para validación, required para obligatorios, default para opcionales. validator para reglas custom. Las props son readonly — nunca mutarlas directamente. Con TypeScript, usar generics para tipado completo. Acceso vía props.x en el script, directo en el template.

Async components
<script setup>
import { defineAsyncComponent } from "vue";

// Lazy load (code splitting):
const HeavyEditor = defineAsyncComponent(
  () => import("./HeavyEditor.vue")
);

// Con loading y error:
const Panel = defineAsyncComponent({
  loader: () => import("./Panel.vue"),
  loadingComponent: Spinner,
  errorComponent: ErrorMsg,
  delay: 200,      // ms antes de mostrar loading
  timeout: 10000,  // ms antes de error
});
</script>

<template>
  <HeavyEditor v-if="showEditor" />
  <Suspense>
    <Panel />
    <template #fallback><p>Cargando...</p></template>
  </Suspense>
</template>

defineAsyncComponent() carga componentes bajo demanda (code splitting). Reduce el bundle inicial. loadingComponent muestra un spinner durante la carga. errorComponent para fallos. delay evita el flash en cargas rápidas. Suspense (experimental) para fallback declarativo. Ideal para componentes pesados (editors, charts, modales raros).

Renderless components
<!-- useMouse.vue (renderless): -->
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const x = ref(0);
const y = ref(0);

function update(e) { x.value = e.pageX; y.value = e.pageY; }
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
</script>

<template>
  <slot :x="x" :y="y" />
</template>

<!-- Uso: -->
<UseMouse v-slot="{ x, y }">
  <p>Ratón: {{ x }}, {{ y }}</p>
</UseMouse>

<!-- Otro consumo (layout diferente): -->
<UseMouse v-slot="{ x, y }">
  <div :style="{ left: x+'px', top: y+'px' }" class="cursor" />
</UseMouse>

Renderless component: componente sin markup propio — solo lógica + scoped slot. Separa la lógica de la renderización. El consumidor decide cómo mostrar vía slot. Alternativa a composables (cuando necesita template). Patrón popular en bibliotecas (VueUse, Headless UI). Hoy, los composables son preferidos en la mayoría de los casos.

Pasar props (padre→hijo)
<template>
  <!-- Estático (string literal): -->
  <Card title="Hola Mundo" />

  <!-- Dinámico (expresión, usa :): -->
  <Card :title="variableName" />
  <Card :value="10 + 5" />
  <Card :items="filteredList" />
  <Card :active="true" />

  <!-- Sin : siempre es string: -->
  <Card value="10" />   <!-- string "10" -->
  <Card :value="10" />  <!-- número 10 -->

  <!-- Spread de objeto: -->
  <Card v-bind="objProps" />
  <!-- equivale a pasar cada propiedad -->
</template>

<script setup>
const objProps = { title: "X", value: 5, active: true };
</script>

Sin :, el valor es un string literal. Con : (v-bind), es una expresión JavaScript (números, booleanos, arrays, objetos). v-bind="obj" pasa todas las propiedades de un objeto (spread). Las props fluyen unidireccionalmente (padre→hijo). El hijo nunca altera las props — emite un evento para pedir el cambio.

provide / inject
<!-- Ancestro (cualquier nivel arriba): -->
<script setup>
import { provide, ref, readonly } from "vue";

const theme = ref("dark");
provide("theme", readonly(theme));
provide("setTheme", (val) => { theme.value = val; });
</script>

<!-- Descendiente (cualquier nivel abajo): -->
<script setup>
import { inject } from "vue";

const theme = inject("theme");
const setTheme = inject("setTheme");
// Con default:
const lang = inject("language", "es");
</script>

<template>
  <p>Tema: {{ theme }}</p>
  <button @click="setTheme('light')">Cambiar</button>
</template>

provide/inject pasa datos entre cualquier nivel del árbol (sin prop drilling). El ancestro hace provide("clave", valor); el descendiente inject("clave"). Exponer readonly() + método de mutación para control. Segundo parámetro de inject = default. Ideal para temas, locale, auth. Para estado complejo, preferir Pinia.

Patrones de composición
<!-- Wrapper component (proxy): -->
<!-- MyInput.vue: -->
<script setup>
defineProps(["modelValue", "label", "error"]);
defineEmits(["update:modelValue"]);
</script>
<template>
  <div class="field">
    <label>{{ label }}</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      :class="{ error }"
    />
    <span v-if="error" class="msg">{{ error }}</span>
  </div>
</template>

<!-- Uso: -->
<MyInput v-model="name" label="Nombre" :error="errors.name" />
<MyInput v-model="email" label="Email" :error="errors.email" />

Los wrapper components encapsulan markup + lógica repetitiva. Proxy de v-model (recibe y re-emite). Props para configuración (label, error). Slots para flexibilidad extra. Reduce la duplicación en formularios. Patrón: input, select, modal, card. Mantener la API simple (pocas props). Componer con slots para casos especiales.

Slots avanzados
<!-- Modal.vue: -->
<template>
  <div class="modal">
    <header>
      <slot name="title">Título por defecto</slot>
      <button @click="$emit('close')">✕</button>
    </header>
    <main><slot /></main>
    <footer>
      <slot name="actions">
        <button @click="$emit('close')">Cerrar</button>
      </slot>
    </footer>
  </div>
</template>

<!-- Uso: -->
<Modal @close="visible = false">
  <template #title><h2>Confirmar</h2></template>
  <p>¿Estás seguro?</p>
  <template #actions>
    <button @click="confirm">Sí</button>
    <button @click="visible = false">No</button>
  </template>
</Modal>

Los slots con nombre crean layouts flexibles. <slot name="x"> define el área; #x la rellena. Slot default (sin nombre) para contenido principal. Fallback (contenido dentro del slot) si no se rellena. Slots + emits = componente totalmente personalizable. Patrón base de design systems (modales, cards, layouts).

Componente recursivo
<!-- TreeItem.vue: -->
<script setup>
defineProps({
  node: { type: Object, required: true }
});
</script>

<template>
  <li>
    <span>{{ node.name }}</span>
    <!-- Recursión: el componente se usa a sí mismo -->
    <ul v-if="node.children?.length">
      <TreeItem
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </ul>
  </li>
</template>

<!-- Uso: -->
<ul>
  <TreeItem :node="root" />
</ul>

Los componentes pueden referenciarse a sí mismos (recursión). En script setup, el nombre del archivo es el nombre del componente. Esencial para árboles, menús anidados y comentarios con replies. Siempre tener una condición de parada (v-if) para evitar un bucle infinito. :key obligatorio en el v-for recursivo.

Composition API


10 cards
Composables (use*)
// composables/useCounter.js:
import { ref, computed } from "vue";

export function useCounter(initial = 0) {
  const n = ref(initial);
  const double = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  function reset() { n.value = initial; }

  return { n, double, inc, dec, reset };
}

// Uso:
<script setup>
import { useCounter } from "@/composables/useCounter";
const { n, double, inc, dec, reset } = useCounter(10);
</script>

Los composables son funciones que encapsulan lógica reactiva reutilizable. Convención: prefijo use. Devuelven refs, computed y métodos. Cada llamada crea estado independiente. Sustituyen a los mixins de Vue 2 (sin conflictos de nombre). Pueden usar lifecycle hooks internamente. Base de la Composition API — organizar por funcionalidad, no por tipo.

useSlots y useAttrs
<script setup>
import { useSlots, useAttrs } from "vue";

const slots = useSlots();
const attrs = useAttrs();

// Verificar si un slot existe:
const hasFooter = computed(() => !!slots.footer);

// Acceder a attrs:
console.log(attrs.class);
console.log(attrs["data-id"]);
</script>

<template>
  <div v-bind="attrs">
    <slot />
    <footer v-if="hasFooter">
      <slot name="footer" />
    </footer>
  </div>
</template>

useSlots() da acceso programático a los slots (verificar existencia, renderizar condicionalmente). useAttrs() da acceso a atributos/eventos no declarados como props. Equivalentes a $slots y $attrs del template. Útiles en componentes wrapper que necesitan lógica basada en slots/attrs. computed(() => !!slots.x) para condicional reactivo.

VueUse (biblioteca de composables)
// npm i @vueuse/core
<script setup>
import {
  useMouse,
  useLocalStorage,
  useDark,
  useMediaQuery,
  useEventListener,
  useDebounceFn,
  useIntersectionObserver
} from "@vueuse/core";

// Posición del ratón:
const { x, y } = useMouse();

// Dark mode:
const isDark = useDark();

// LocalStorage reactivo:
const token = useLocalStorage("token", "");

// Media query:
const isMobile = useMediaQuery("(max-width: 768px)");

// Debounce:
const debouncedSearch = useDebounceFn(doSearch, 300);
</script>

VueUse es la mayor biblioteca de composables Vue (200+ funciones). Cubre: browser APIs, sensores, estado, animaciones, utilities. Todos son composables reactivos con cleanup automático. Sustituye código boilerplate repetitivo. useLocalStorage, useDark, useMouse son los más populares. Documentación excelente con demos interactivas.

Composable con lifecycle
// composables/useFetch.js:
import { ref, watchEffect } from "vue";

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);

  async function load() {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url.value ?? url);
      data.value = await res.json();
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  }

  // Si url es un ref, re-fetch al cambiar:
  if (typeof url === "object") {
    watchEffect(load);
  } else {
    load();
  }

  return { data, error, loading, refetch: load };
}

Los composables pueden contener lifecycle hooks y watchers internamente. useFetch es el ejemplo clásico: loading, error, data + refetch. Si la URL es un ref, re-fetch automático vía watchEffect. Devolver refetch para recargar manualmente. Patrón replicable: useLocalStorage, useInterval, useMediaQuery.

watch con deep e immediate
<script setup>
import { reactive, watch } from "vue";

const form = reactive({ name: "", email: "", address: { street: "", city: "" } });

// deep: detecta cambios internos (nested)
watch(form, (val) => {
  saveDraft(val);
}, { deep: true });

// immediate: ejecuta al crear (no solo en cambios)
watch(() => form.email, async (email) => {
  if (email) await checkEmail(email);
}, { immediate: true });

// Combinar:
watch(form, handler, { deep: true, immediate: true });

// Getter como fuente (más eficiente que deep):
watch(() => form.address.city, (city) => {
  updateDistrict(city);
});
</script>

deep: true observa cambios en propiedades nested (coste: recorre todo). immediate: true ejecuta el callback inmediatamente (no espera el 1er cambio). Preferir un getter (() => obj.prop) a deep cuando solo necesita una propiedad. Deep en objetos grandes es costoso — considerar watchers específicos por campo.

Testear composables
// composables/useCounter.js (¡testeable!)
export function useCounter(initial = 0) {
  const n = ref(initial);
  const inc = () => n.value++;
  return { n, inc };
}

// useCounter.spec.js (Vitest):
import { describe, it, expect } from "vitest";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  it("inicia con el valor dado", () => {
    const { n } = useCounter(5);
    expect(n.value).toBe(5);
  });

  it("incrementa", () => {
    const { n, inc } = useCounter(0);
    inc();
    inc();
    expect(n.value).toBe(2);
  });
});

// Ejecutar: npx vitest

Los composables son funciones puras — testeables sin componente. Importar y llamar directamente en el test. Vitest es el test runner oficial (compatible con Jest). No necesita mount/render para testear lógica. Para composables con lifecycle, usar @vue/test-utils con wrapper. Ventaja sobre mixins: aislamiento total, sin conflictos.

Composable con cleanup
// composables/useInterval.js:
import { ref, onUnmounted } from "vue";

export function useInterval(callback, ms) {
  const active = ref(true);
  let id = null;

  function start() {
    if (id) return;
    id = setInterval(callback, ms);
    active.value = true;
  }

  function stop() {
    clearInterval(id);
    id = null;
    active.value = false;
  }

  start();
  onUnmounted(stop);  // ¡cleanup automático!

  return { active, start, stop };
}

// Uso:
<script setup>
import { useInterval } from "./useInterval";
const { active, stop } = useInterval(() => {
  seconds.value++;
}, 1000);
</script>

Los composables deben hacer cleanup en onUnmounted (listeners, intervals, subscriptions). Evita memory leaks cuando el componente se destruye. onUnmounted dentro del composable se liga al ciclo de vida del componente que lo usa. Devolver controles (stop, start) para gestión manual. Patrón obligatorio para recursos externos.

Computed vs watch vs watchEffect
// computed: derivar valor (con caché)
const total = computed(() => price.value * qty.value);

// watch: side effect cuando cambia una fuente específica
watch(userId, (id) => { fetchUser(id); });

// watchEffect: side effect con deps automáticas
watchEffect(() => {
  console.log(counter.value, name.value);
  // re-ejecuta si cualquiera cambia
});

// Reglas:
// ¿Necesita valor derivado? → computed
// ¿Necesita el valor antiguo? → watch (newVal, oldVal)
// ¿Side effect simple? → watchEffect
// ¿Fetch asíncrono? → watch (más control)

computed: valor derivado con caché (no ejecutar side effects). watch: side effect con fuente explícita + acceso al valor antiguo. watchEffect: side effect con dependencias automáticas (ejecuta ya). computed es síncrono y cacheado; watch/watchEffect son para efectos (fetch, DOM, logging). Elegir según la necesidad: valor → computed; efecto → watch/watchEffect.

Organización por feature
// En vez de Options API (agrupado por tipo):
// data(), methods, computed, watch — todo separado

// Composition API (agrupado por feature):
<script setup>
// Feature: Búsqueda
const search = ref("");
const results = ref([]);
const canSearch = computed(() => search.value.length > 2);
watch(search, doSearch);

// Feature: Paginación
const page = ref(1);
const totalPages = ref(1);
function next() { page.value++; }

// Feature: Selección
const selected = ref([]);
function toggle(item) { /* ... */ }
</script>

// O extraer a composables:
// const { search, results } = useSearch();
// const { page, next } = usePagination();

La Composition API permite agrupar el código por funcionalidad (no por tipo). Todo lo relacionado queda junto — más legible en componentes grandes. Extraer a composables cuando sea reutilizable. Contrasta con la Options API donde data/methods/computed quedan separados. Escala mejor: cada feature es independiente y testeable.

Reactividad con Generics (TS)
<script setup lang="ts">
import { ref, computed } from "vue";

interface User {
  id: number;
  name: string;
  email: string;
}

// Tipado explícito:
const users = ref<User[]>([]);
const selected = ref<User | null>(null);

// Inferencia automática:
const counter = ref(0);  // Ref<number>
const name = ref("Ana");  // Ref<string>

// Computed tipado:
const names = computed(() =>
  users.value.map(u => u.name)
);  // ComputedRef<string[]>

// Función genérica:
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
</script>

TypeScript con Vue 3: ref<T>() para tipado explícito. Inferencia automática para primitivos. computed infiere el tipo del retorno. Interfaces para modelos de datos. defineProps<T>() y defineEmits<T>() con generics. Volar (extensión de VS Code) da type-checking en templates. Vue 3 fue reescrito en TypeScript.

Ciclo de Vida


9 cards
Hooks (Composition API)
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from "vue";

onBeforeMount(() => {
  // Antes de renderizar (sin DOM)
});

onMounted(() => {
  // DOM disponible — acceder a elementos
});

onBeforeUpdate(() => {
  // Antes de re-renderizar
});

onUpdated(() => {
  // Después de re-renderizar (DOM actualizado)
});

onBeforeUnmount(() => {
  // Antes de destruir (cleanup)
});

onUnmounted(() => {
  // Destruido — eliminar listeners
});
</script>

Hooks de ciclo de vida en la Composition API: importar de "vue" y registrar con callback. onMounted es el más usado (DOM listo, fetch inicial). onUnmounted para cleanup (listeners, intervals). Solo funcionan dentro de setup() o <script setup>. Se permiten múltiples hooks del mismo tipo (ejecutan en orden).

onUnmounted — cleanup
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

let intervalId = null;
const seconds = ref(0);

onMounted(() => {
  intervalId = setInterval(() => {
    seconds.value++;
  }, 1000);

  window.addEventListener("resize", onResize);
});

onUnmounted(() => {
  clearInterval(intervalId);
  window.removeEventListener("resize", onResize);
});

function onResize() { /* ... */ }
</script>

onUnmounted es esencial para cleanup: intervals, timeouts, event listeners, subscriptions, WebSocket. Sin cleanup = memory leaks. Registrar en onMounted, limpiar en onUnmounted. Los composables hacen esto automáticamente. En una SPA, los componentes se destruyen al navegar — el cleanup es crítico.

Suspense (async setup)
<!-- Hijo con async setup: -->
<script setup>
// Top-level await (componente asíncrono):
const data = await fetch("/api/data").then(r => r.json());
</script>

<!-- Padre con Suspense: -->
<template>
  <Suspense>
    <!-- Contenido principal (async): -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- Fallback durante loading: -->
    <template #fallback>
      <Spinner />
    </template>
  </Suspense>

  <!-- Error (con onErrorCaptured o @error): -->
</template>

<Suspense> (experimental) muestra un fallback mientras los componentes async cargan. Un componente con top-level await en el setup se vuelve asíncrono. #default = contenido; #fallback = loading. Múltiples hijos async: Suspense espera a todos. Aún experimental — la API puede cambiar. Alternativa estable: defineAsyncComponent + loading state manual.

Hooks (Options API)
<script>
export default {
  data() {
    return { data: null };
  },

  created() {
    // Datos listos, SIN DOM
    // Bueno para: fetch inicial, init state
  },

  mounted() {
    // DOM disponible
    // Bueno para: refs, mediciones, libs externas
    console.log(this.$el);  // elemento raíz
  },

  updated() {
    // Después del re-render
  },

  unmounted() {
    // Destruido — cleanup
    clearInterval(this.timer);
  }
};
</script>

En la Options API, los hooks son métodos del objeto: created, mounted, updated, unmounted. created no tiene equivalente directo en la Composition API (el código en el top-level del setup ejecuta en esa fase). this da acceso a datos, métodos y $el. Prefijo before para la fase anterior.

activated / deactivated (KeepAlive)
<script setup>
import { onActivated, onDeactivated } from "vue";

// Solo funciona dentro de <KeepAlive>:
onActivated(() => {
  // Componente reactivado (volvió al DOM)
  console.log("Visible de nuevo");
  refreshData();
});

onDeactivated(() => {
  // Componente desactivado (salió del DOM pero está en caché)
  console.log("Oculto (en caché)");
  stopTimer();
});
</script>

<!-- Padre: -->
<template>
  <KeepAlive>
    <component :is="currentView" />
  </KeepAlive>
</template>

onActivated/onDeactivated solo disparan en componentes dentro de <KeepAlive>. KeepAlive mantiene el componente en caché (no lo destruye). activated = volvió a ser visible; deactivated = fue ocultado (pero existe). Útil para pausar timers, refrescar datos al volver. onMounted solo dispara 1 vez con KeepAlive.

onMounted — cargar datos
<script setup>
import { ref, onMounted } from "vue";

const users = ref([]);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const res = await fetch("/api/users");
    users.value = await res.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <p v-if="loading">Cargando...</p>
  <p v-else-if="error">Error: {{ error }}</p>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

Patrón clásico: onMounted + async/await para el fetch inicial. Estado triple: loading, error, data. finally garantiza que loading termina. Template con v-if/v-else-if/v-else para cada estado. Alternativa: composable useFetch() que encapsula todo. Nunca fetch en el top-level del setup (sin SSR safety).

onErrorCaptured
<script setup>
import { onErrorCaptured, ref } from "vue";

const error = ref(null);

// Capturar errores de componentes hijos:
onErrorCaptured((err, instance, info) => {
  error.value = err.message;
  console.error("Error en:", instance?.$options.name);
  console.error("Info:", info);

  return false;  // no propagar hacia arriba
  // return true → propaga al padre/ancestros
});
</script>

<template>
  <div v-if="error" class="error">
    Algo falló: {{ error }}
  </div>
  <ChildComponent v-else />
</template>

onErrorCaptured intercepta errores de descendientes (error boundary). Recibe (error, instancia, info). return false detiene la propagación. Patrón: mostrar fallback UI en vez de crashear. Equivalente a los Error Boundaries de React. Para errores globales: app.config.errorHandler. Info indica dónde ocurrió (render, watcher, hook).

nextTick
<script setup>
import { ref, nextTick } from "vue";

const message = ref("Hola");
const inputRef = ref(null);

async function update() {
  message.value = "¡Actualizado!";

  // El DOM AÚN no refleja el cambio:
  console.log(inputRef.value.textContent); // "Hola"

  // Esperar el próximo tick (DOM actualizado):
  await nextTick();
  console.log(inputRef.value.textContent); // "¡Actualizado!"
}

// Alternativa con callback:
nextTick(() => {
  // DOM actualizado
});
</script>

Vue actualiza el DOM de forma asíncrona (batch de cambios). nextTick() espera el próximo ciclo de renderizado. Necesario cuando necesita el DOM actualizado tras cambiar datos. Devuelve una Promise (usar await) o acepta callback. Casos: focus tras v-if, mediciones tras update, scroll tras inserción.

Orden de ejecución
// Ciclo completo (padre + hijo):

// Creación:
// 1. setup() del Padre
// 2. beforeMount del Padre
// 3. setup() del Hijo
// 4. beforeMount del Hijo
// 5. mounted del Hijo  ← el hijo monta 1º
// 6. mounted del Padre

// Update:
// 1. beforeUpdate del Padre
// 2. beforeUpdate del Hijo
// 3. updated del Hijo
// 4. updated del Padre

// Destrucción:
// 1. beforeUnmount del Padre
// 2. beforeUnmount del Hijo
// 3. unmounted del Hijo
// 4. unmounted del Padre

// Regla: los hijos completan antes que el padre

Orden: setup → beforeMount → mounted (creación); beforeUpdate → updated (cambio); beforeUnmount → unmounted (destrucción). Los hijos completan antes que el padre en mount/unmount. setup() ejecuta antes que todos los hooks. Código en el top-level de <script setup> = fase "created". Importante para dependencias entre padre/hijo.

Avançado


10 cards
Teleport
<template>
  <button @click="open = true">Abrir Modal</button>

  <!-- Renderiza en <body>, no aquí: -->
  <Teleport to="body">
    <div v-if="open" class="overlay" @click="open = false">
      <div class="modal" @click.stop>
        <h2>Modal</h2>
        <p>Contenido del modal</p>
        <button @click="open = false">Cerrar</button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from "vue";
const open = ref(false);
</script>

<style>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); }
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); }
</style>

<Teleport to="body"> renderiza contenido en otro lugar del DOM (fuera de la jerarquía del componente). Mantiene la lógica en el componente (reactividad, eventos). Ideal para modales, tooltips, dropdowns (evita el overflow:hidden y z-index de los ancestros). to acepta un selector CSS. Múltiples Teleports al mismo destino se van añadiendo.

v-model múltiples
<!-- Padre: -->
<UserForm
  v-model:name="form.name"
  v-model:email="form.email"
  v-model:age="form.age"
/>

<!-- Hijo (UserForm.vue): -->
<script setup>
const props = defineProps(["name", "email", "age"]);
const emit = defineEmits([
  "update:name",
  "update:email",
  "update:age"
]);
</script>

<template>
  <input :value="name"
    @input="emit('update:name', $event.target.value)" />
  <input :value="email"
    @input="emit('update:email', $event.target.value)" />
  <input :value="age" type="number"
    @input="emit('update:age', +$event.target.value)" />
</template>

Vue 3 permite múltiples v-model con nombre en un componente. Cada uno es un par :prop + @update:prop. Sustituye el modificador .sync de Vue 2. Nombres en camelCase en el script, kebab-case en el template. Ideal para formularios complejos con varios campos. Cada v-model es independiente y bidireccional.

Performance y optimización
<!-- 1. v-once (renderizar 1 vez): -->
<p v-once>{{ staticContent }}</p>

<!-- 2. v-memo (memoizar por dependencia): -->
<div v-for="item in list" :key="item.id"
  v-memo="[item.selected]">
  {{ item.name }} — {{ item.selected }}
</div>

<!-- 3. shallowRef para datos grandes: -->
<script setup>
import { shallowRef } from "vue";
const bigData = shallowRef([]);
// Sustituir entero (no mutar nested):
bigData.value = await fetchData();
</script>

<!-- 4. Lazy components: -->
const Heavy = defineAsyncComponent(
  () => import("./Heavy.vue")
);

<!-- 5. Virtual scroll para listas largas -->
<!-- 6. computed en vez de métodos en el template -->

Optimizaciones Vue: v-once para contenido estático. v-memo para saltar re-renders en listas. shallowRef para datos grandes (sin tracking profundo). defineAsyncComponent para code splitting. computed (con caché) en vez de métodos en el template. Virtual scroll para +1000 ítems. Vue DevTools Profiler para identificar cuellos de botella.

Transition
<template>
  <Transition name="fade">
    <p v-if="visible">¡Hola!</p>
  </Transition>

  <Transition name="slide">
    <div v-show="open" class="panel">Contenido</div>
  </Transition>
</template>

<style>
/* Clases generadas automáticamente: */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

.slide-enter-active, .slide-leave-active {
  transition: transform 0.3s, opacity 0.3s;
}
.slide-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}
.slide-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

<Transition> aplica animaciones CSS cuando los elementos entran/salen del DOM. Clases automáticas: -enter-from, -enter-active, -enter-to, -leave-from, -leave-active, -leave-to. Prefijo = atributo name. Funciona con v-if y v-show. Para listas: <TransitionGroup>. También acepta JavaScript hooks.

Directiva custom (global)
// main.js — registrar globalmente:
app.directive("tooltip", {
  mounted(el, binding) {
    el.title = binding.value;
    el.style.cursor = "help";
  },
  updated(el, binding) {
    el.title = binding.value;
  }
});

// directives/vClickOutside.js:
export const vClickOutside = {
  mounted(el, binding) {
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value();
    };
    document.addEventListener("click", el._handler);
  },
  unmounted(el) {
    document.removeEventListener("click", el._handler);
  }
};

// Uso:
// <div v-tooltip="'Ayuda'">?</div>
// <div v-click-outside="close">...</div>

Directivas globales vía app.directive(). Hooks: mounted, updated, unmounted. binding.value es el valor pasado. binding.arg y binding.modifiers para configuración extra. Cleanup en unmounted (eliminar listeners). Usar para: tooltips, click-outside, lazy-load, focus. Preferir composables cuando sea posible.

SSR y Nuxt
// Nuxt 3 (framework SSR sobre Vue):
// npx nuxi@latest init mi-app

// Estructura Nuxt:
// pages/       → rutas automáticas
// components/  → auto-import
// composables/ → auto-import
// server/      → API routes (Nitro)
// app.vue      → layout raíz

// pages/index.vue:
<script setup>
// useFetch: SSR-safe (ejecuta en el servidor)
const { data } = await useFetch("/api/posts");
</script>

<template>
  <div v-for="post in data" :key="post.id">
    {{ post.title }}
  </div>
</template>

// Ventajas: SEO, performance (FCP), code splitting

Nuxt 3 es el framework SSR/fullstack para Vue. SSR (Server-Side Rendering): HTML generado en el servidor (mejor SEO y FCP). useFetch es SSR-safe (ejecuta en servidor + cliente). Auto-imports de componentes y composables. server/ para API routes. Alternativa SPA pura: Vite + Vue Router. Elegir Nuxt para SEO y performance.

TransitionGroup (listas)
<template>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
      <button @click="remove(item.id)">✕</button>
    </li>
  </TransitionGroup>
</template>

<style>
.list-enter-active, .list-leave-active {
  transition: all 0.3s;
}
.list-enter-from, .list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
/* Animación de movimiento (FLIP): */
.list-move {
  transition: transform 0.3s;
}
/* Necesario para que leave funcione: */
.list-leave-active {
  position: absolute;
}
</style>

<TransitionGroup> anima inserción, eliminación y reordenación de listas. tag="ul" define el elemento wrapper. :key obligatorio. La clase -move anima el reposicionamiento (FLIP animation). position: absolute en leave-active para no afectar el layout. Ideal para listas con add/remove/reorder. Sin jQuery — todo CSS.

Render functions y JSX
<script setup>
import { h } from "vue";

// Render function (alternativa a templates):
const render = () => h("div", { class: "box" }, [
  h("h1", "Título"),
  h("p", { onClick: () => alert("hola") }, "Párrafo")
]);

// Componente funcional:
function MyTitle(props) {
  return h("h" + props.level, props.text);
}

// Con JSX (plugin @vitejs/plugin-vue-jsx):
// const Comp = () => (
//   <div class="box">
//     <h1>Título</h1>
//     {items.map(i => <li key={i.id}>{i.name}</li>)}
//   </div>
// );
</script>

h() crea VNodes programáticamente (alternativa a templates). Útil para componentes altamente dinámicos. JSX disponible con plugin (sintaxis React-like). Los templates se compilan a render functions. Preferir templates en la mayoría de los casos (más legible, optimizaciones del compilador). Render functions para lógica compleja de renderizado.

KeepAlive
<template>
  <!-- Caché de componentes dinámicos: -->
  <KeepAlive :include="['TabA', 'TabB']" :max="5">
    <component :is="currentComponent" />
  </KeepAlive>

  <!-- include: solo estos se cachean -->
  <!-- exclude: estos NO se cachean -->
  <!-- max: máximo de instancias en caché -->
</template>

<script setup>
import { shallowRef } from "vue";
import TabA from "./TabA.vue";
import TabB from "./TabB.vue";

const currentComponent = shallowRef(TabA);
</script>

<!-- En el componente cacheado: -->
<!-- onActivated() / onDeactivated() -->

<KeepAlive> mantiene componentes en caché en vez de destruirlos al cambiar. Preserva estado (scroll, inputs, datos). include/exclude filtran por nombre. max limita instancias (LRU eviction). Los componentes cacheados reciben onActivated/onDeactivated. Ideal para tabs, wizards y navegación con estado.

Error boundaries
<!-- ErrorBoundary.vue: -->
<script setup>
import { ref, onErrorCaptured } from "vue";

const error = ref(null);
const info = ref("");

onErrorCaptured((err, instance, inf) => {
  error.value = err;
  info.value = inf;
  return false;  // no propagar
});

function retry() {
  error.value = null;
}
</script>

<template>
  <slot v-if="!error" />
  <div v-else class="error-boundary">
    <h3>Algo salió mal</h3>
    <p>{{ error.message }}</p>
    <button @click="retry">Intentar de nuevo</button>
  </div>
</template>

<!-- Uso: -->
<ErrorBoundary>
  <ComponentThatMayFail />
</ErrorBoundary>

Patrón Error Boundary: componente wrapper con onErrorCaptured. Captura errores de todos los descendientes. Muestra fallback UI en vez de crashear la app. return false impide la propagación. Botón retry para intentar de nuevo. Envolver secciones críticas (widgets, contenido externo). Para errores globales: app.config.errorHandler.

Reatividade


11 cards
ref() — valor reactivo
<script setup>
import { ref } from "vue";

// Primitivos:
const counter = ref(0);
const name = ref("Ana");
const active = ref(true);

// En el script: usar .value
counter.value++;
console.log(counter.value); // 1

// En el template: sin .value
// {{ counter }} → 1
// {{ name }} → "Ana"

// Arrays y objetos también:
const items = ref([1, 2, 3]);
const user = ref({ name: "Ana", age: 30 });
items.value.push(4);
user.value.age = 31;
</script>

ref() crea un valor reactivo envuelto en un objeto con .value. En el template, el .value es automático (unwrap). Funciona con cualquier tipo: primitivos, arrays, objetos. Alterar .value dispara un re-render. Es la API más versátil — funciona en todas partes (script, template, composables).

watch() — observar cambios
<script setup>
import { ref, watch } from "vue";

const search = ref("");
const userId = ref(1);

// Watch simple:
watch(search, (newVal, oldVal) => {
  console.log(`Cambió: "${oldVal}" → "${newVal}"`);
  doSearch(newVal);
});

// Watch con opciones:
watch(userId, async (id) => {
  data.value = await fetchUser(id);
}, { immediate: true });  // se ejecuta ya

// Múltiples fuentes:
watch([search, userId], ([s, id], [prevS, prevId]) => {
  console.log("Algo cambió");
});

// Parar watcher:
const stop = watch(search, fn);
stop();  // cancela

watch() ejecuta un callback cuando una fuente reactiva cambia. Recibe (nuevo, viejo). Opciones: immediate: true (se ejecuta ya), deep: true (cambios internos en objetos). Acepta múltiples fuentes en array. Retorna función stop() para cancelar. Ideal para side effects: fetch, logging, sincronización. No usar para derivar valores (usar computed).

readonly y shallowReadonly
<script setup>
import { reactive, readonly, ref } from "vue";

const original = reactive({ n: 0, list: [1] });
const copy = readonly(original);

copy.n = 10;  // ⚠️ ¡Warning! No cambia.
original.n = 10;  // OK → copy.n también cambia

// Con ref:
const count = ref(0);
const readOnly = readonly(count);

// Patrón: exponer readonly desde un composable
function useCounter() {
  const n = ref(0);
  const inc = () => n.value++;
  return {
    n: readonly(n),  // el exterior no altera
    inc              // solo vía método
  };
}
</script>

readonly() crea un proxy inmutable — las alteraciones disparan un warning en dev. Los cambios en el original se reflejan en el readonly (es una vista). Patrón: los composables exponen readonly() para encapsulación (el estado solo cambia vía métodos proporcionados). shallowReadonly() solo protege el primer nivel. Útil para prevenir mutaciones accidentales.

reactive() — objeto reactivo
<script setup>
import { reactive } from "vue";

const state = reactive({
  name: "Ana",
  age: 30,
  preferences: {
    theme: "dark",
    language: "es"
  }
});

// Sin .value:
state.age++;
state.preferences.theme = "light";

// CUIDADO: no desestructurar (pierde reactividad):
const { name } = state;  // ← ¡NO reactivo!

// Sustituir el objeto entero (pierde reactividad):
// state = { name: "X" };  ← ¡ERROR!
Object.assign(state, { name: "X" });  // OK
</script>

reactive() hace un objeto profundamente reactivo (vía Proxy). No necesita .value — acceso directo a las propiedades. Limitaciones: solo funciona con objetos (no primitivos); la desestructuración pierde reactividad; no reasignar el objeto entero. Para primitivos o necesidad de reasignación, usar ref().

watchEffect() — efecto automático
<script setup>
import { ref, watchEffect } from "vue";

const url = ref("/api/users");
const data = ref(null);

// Rastrea dependencias automáticamente:
const stop = watchEffect(async () => {
  // url.value se rastrea como dependencia
  const res = await fetch(url.value);
  data.value = await res.json();
});

// Cuando url.value cambie, se re-ejecuta

// Con cleanup:
watchEffect((onCleanup) => {
  const controller = new AbortController();
  fetch(url.value, { signal: controller.signal });

  onCleanup(() => controller.abort());
});

// Parar:
stop();
</script>

watchEffect() se ejecuta inmediatamente y rastrea dependencias automáticamente (sin especificar fuente). Se re-ejecuta cuando cualquier reactivo usado dentro cambia. onCleanup cancela efectos anteriores (evitar race conditions en fetch). Más simple que watch() cuando no necesitas el valor antiguo. Retorna stop(). Ideal para efectos colaterales reactivos.

Reactividad en profundidad
<script setup>
import { ref, isRef, unref, isReactive } from "vue";

const data = ref({ user: { name: "Ana" } });

// Profundamente reactivo por defecto:
data.value.user.name = "Maria";  // dispara render

// Verificar tipos:
isRef(data);           // true
isReactive(data.value); // true (objeto interno)

// unref: obtener valor (ref o no):
const val = unref(data);  // si ref → .value, si no → él mismo

// customRef: control total
import { customRef } from "vue";
function useDebouncedRef(value, delay = 300) {
  let timeout;
  return customRef((track, trigger) => ({
    get() { track(); return value; },
    set(v) {
      clearTimeout(timeout);
      timeout = setTimeout(() => { value = v; trigger(); }, delay);
    }
  }));
}
</script>

ref() y reactive() son profundamente reactivos por defecto (nested objects). isRef()/isReactive() verifican tipos. unref() extrae el valor de un ref o devuelve el propio valor. customRef() permite control total de tracking/triggering (ej: debounce). Vue usa Proxy (ES6) para detectar cambios — no necesita Vue.set() como Vue 2.

ref vs reactive
// ref: cualquier tipo, .value necesario
const n = ref(0);
const list = ref([]);
n.value = 10;
list.value = [1, 2, 3]; // reasignación OK

// reactive: solo objetos, sin .value
const state = reactive({ n: 0, list: [] });
state.n = 10;
state.list = [1, 2, 3]; // OK (propiedad)

// Cuándo usar cada uno:
// ref → primitivos, arrays, reasignación
// reactive → formularios, estado agrupado

// Regla práctica: usar ref() por defecto
// reactive() para objetos que nunca se sustituyen

ref() es más versátil: cualquier tipo, permite reasignación, funciona en composables. reactive() es más conveniente para objetos (sin .value) pero tiene limitaciones. Regla práctica: ref() por defecto; reactive() para formularios/estado agrupado que nunca se sustituye. Ambos son profundamente reactivos.

shallowRef y shallowReactive
<script setup>
import { shallowRef, shallowReactive, triggerRef } from "vue";

// shallowRef: solo .value es reactivo (no profundo)
const list = shallowRef([1, 2, 3]);
list.value.push(4);  // ¡NO dispara render!
list.value = [...list.value, 4];  // OK (reasignación)

// Forzar update manual:
triggerRef(list);

// shallowReactive: solo primer nivel
const state = shallowReactive({
  user: { name: "Ana" }  // nested NO reactivo
});
state.user.name = "X";  // ¡NO dispara!
state.user = { name: "X" };  // OK
</script>

shallowRef() / shallowReactive() son versiones no profundas — solo el primer nivel es reactivo. Rendimiento: evitar tracking profundo en grandes estructuras. triggerRef() fuerza un update manual. Usar para datos grandes que cambian por reasignación (listas de API, tablas). Para reactividad anidada, usar ref() normal.

Patrones de reactividad
<script setup>
import { ref, computed, watch } from "vue";

// 1. Estado + computed + watch:
const items = ref([]);
const total = computed(() => items.value.length);
const filtered = computed(() =>
  items.value.filter(i => i.active)
);

// 2. Loading state:
const loading = ref(false);
const error = ref(null);
const data = ref(null);

async function load() {
  loading.value = true;
  error.value = null;
  try {
    data.value = await fetch("/api").then(r => r.json());
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
}

// 3. Derivado con side effect:
watch(filtered, (list) => {
  localStorage.setItem("items", JSON.stringify(list));
});
</script>

Patrones comunes: ref para estado, computed para derivados, watch para side effects. Loading state (loading, error, data) para async. Computed para filtros/transformaciones (con caché). Watch para persistencia, logging, sincronización externa. Separar responsabilidades: datos → computed → effects.

computed() — valores derivados
<script setup>
import { ref, computed } from "vue";

const price = ref(100);
const qty = ref(3);
const discount = ref(0.1);

// Computed (solo lectura, con caché):
const total = computed(() => {
  return price.value * qty.value * (1 - discount.value);
});

// Con getter y setter:
const fullName = computed({
  get: () => `${first.value} ${last.value}`,
  set: (val) => {
    [first.value, last.value] = val.split(" ");
  }
});
</script>

<template>
  <p>Total: {{ total }}€</p>
</template>

computed() deriva valores de otros reactivos con caché — solo recalcula cuando las dependencias cambian. Más eficiente que métodos en el template (que se ejecutan en cada render). Acepta getter/setter para two-way binding. Retorna un ref (usar .value en el script, automático en el template). Ideal para filtros, formateos y cálculos.

toRef y toRefs
<script setup>
import { reactive, toRef, toRefs } from "vue";

const state = reactive({ name: "Ana", age: 30 });

// toRef: crear un ref ligado a una propiedad
const nameRef = toRef(state, "name");
nameRef.value = "Maria";  // ¡actualiza state.name!

// toRefs: desestructurar manteniendo reactividad
const { name, age } = toRefs(state);
name.value = "John";  // ¡actualiza state.name!
age.value = 25;       // ¡actualiza state.age!

// Sin toRefs (pierde la ligación):
// const { name } = state;  ← NO reactivo

// Útil en composables:
function useUser(state) {
  const { name, age } = toRefs(state);
  return { name, age };
}
</script>

toRef() crea un ref bidireccional ligado a una propiedad de un objeto reactivo. toRefs() convierte todas las propiedades en refs (para desestructuración segura). Mantienen la ligación — alterar el ref altera el objeto original. Esencial para desestructurar reactive() o props sin perder reactividad.

Router, Pinia e Ferramentas


11 cards
Vue Router (setup)
// router/index.js:
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/HomeView.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/", component: Home },
    { path: "/about", component: () => import("../views/AboutView.vue") },
    { path: "/users/:id", component: UserDetail, name: "user" },
    { path: "/:pathMatch(.*)*", component: NotFound },
  ],
});

export default router;

// App.vue:
<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
  </nav>
  <RouterView />
</template>

createRouter() configura la navegación. createWebHistory() para URLs limpias (sin #). Rutas con path y component. () => import() para lazy loading. :id para parámetros dinámicos. RouterLink para navegación (sin reload). RouterView renderiza el componente de la ruta activa. Catch-all para 404.

Pinia (acciones y getters)
// stores/cart.js:
export const useCartStore = defineStore("cart", () => {
  const items = ref([]);

  // Getter (computed):
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
  );
  const totalItems = computed(() =>
    items.value.reduce((s, i) => s + i.qty, 0)
  );

  // Acciones (funciones):
  function add(product) {
    const existing = items.value.find(i => i.id === product.id);
    if (existing) { existing.qty++; }
    else { items.value.push({ ...product, qty: 1 }); }
  }

  function remove(id) {
    items.value = items.value.filter(i => i.id !== id);
  }

  async function checkout() {
    await api.post("/orders", { items: items.value });
    items.value = [];
  }

  return { items, total, totalItems, add, remove, checkout };
});

Patrón de store Pinia: ref para estado, computed para getters, funciones para acciones. Las acciones pueden ser async (fetch, persistencia). Los getters son computed (con caché). Desestructuración con storeToRefs() para mantener reactividad. Múltiples stores independientes (cart, auth, ui). Estado global accesible desde cualquier componente.

Vue CLI vs Vite
# Vite (recomendado, actual):
npm create vue@latest
npm run dev     # instantáneo (<300ms)
npm run build   # Rollup (optimizado)

# Vue CLI (legado, Webpack):
npm install -g @vue/cli
vue create mi-app
npm run serve   # lento (30s+ en proyectos grandes)

# Diferencias:
# Vite:
#   - ESM nativo (sin bundling en dev)
#   - HMR instantáneo
#   - Build con Rollup
#   - Configuración mínima

# Vue CLI:
#   - Webpack (bundling total)
#   - HMR lento en proyectos grandes
#   - Más configuración
#   - En mantenimiento (no recomendado para nuevos)

Vite es la build tool por defecto para Vue 3. Dev server con ESM nativo (sin bundling = arranque instantáneo). HMR en milisegundos. Build de producción con Rollup (tree-shaking, code-splitting). Vue CLI (Webpack) está en modo mantenimiento — no usar para proyectos nuevos. Migrar de Vue CLI a Vite es recomendado.

useRoute y useRouter
<script setup>
import { useRoute, useRouter } from "vue-router";

const route = useRoute();   // info de la ruta actual
const router = useRouter(); // métodos de navegación

// Leer parámetros:
const userId = route.params.id;
const search = route.query.q;
const hash = route.hash;

// Navegación programática:
router.push("/users/42");
router.push({ name: "user", params: { id: 42 } });
router.push({ path: "/search", query: { q: "vue" } });
router.replace("/login");  // sin entrada en history
router.back();
router.go(-2);
</script>

<template>
  <p>Ruta: {{ route.path }}</p>
  <p>User: {{ route.params.id }}</p>
</template>

useRoute() da acceso reactivo a la ruta actual (params, query, hash, name). useRouter() da métodos de navegación: push (nueva entrada), replace (sustituye), back/go. params para :id en la path; query para ?q=x. Route es reactivo — cambia al navegar.

storeToRefs y $patch
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";

const store = useCartStore();

// Desestructurar manteniendo reactividad:
const { total, totalItems } = storeToRefs(store);
// Los métodos NO necesitan storeToRefs:
const { add, remove } = store;

// $patch: múltiples cambios atómicos
store.$patch({
  items: [],
  coupon: null
});

// $patch con función (mutaciones complejas):
store.$patch((state) => {
  state.items.push(newItem);
  state.lastUpdate = Date.now();
});

// $reset (solo option stores):
// store.$reset();

// $subscribe: observar cambios
store.$subscribe((mutation, state) => {
  localStorage.setItem("cart", JSON.stringify(state));
});
</script>

storeToRefs() desestructura estado/getters manteniendo reactividad (los métodos no lo necesitan). $patch() aplica múltiples cambios de una vez (1 re-render). $subscribe() observa cambios para persistencia. $state para sustituir el estado entero. $reset() solo en option stores. Patrón: subscribe + localStorage para persistir.

Ecosistema Vue
// Oficial:
// vue-router     → navegación SPA
// pinia          → estado global
// vueuse         → composables utilitarios
// vite           → build tool
// vitest         → tests unitarios
// vue-devtools   → debug en navegador

// UI Libraries:
// Vuetify        → Material Design
// PrimeVue       → 90+ componentes
// Naive UI       → TypeScript-first
// Element Plus   → enterprise
// Quasar         → framework completo

// Herramientas:
// @vue/test-utils → testear componentes
// vite-plugin-pwa → PWA
// unplugin-auto-import → auto-imports
// vite-plugin-pages → rutas automáticas

// Nuxt 3 → SSR/fullstack

Ecosistema Vue maduro: Router + Pinia + VueUse cubren el 90% de los casos. UI libraries: Vuetify (Material), PrimeVue (versátil), Naive UI (TS). Nuxt 3 para SSR/fullstack. Vitest + test-utils para tests. unplugin-auto-import elimina imports manuales. Comunidad activa y documentación excelente.

Rutas anidadas y guards
// Rutas anidadas:
{
  path: "/dashboard",
  component: DashboardLayout,
  children: [
    { path: "", component: DashHome },
    { path: "stats", component: DashStats },
    { path: "settings", component: DashSettings },
  ]
}

// DashboardLayout.vue:
// <RouterView /> renderiza el hijo

// Navigation guards:
router.beforeEach((to, from) => {
  const auth = useAuth();
  if (to.meta.requiresAuth && !auth.loggedIn) {
    return { path: "/login", query: { redirect: to.fullPath } };
  }
});

// Guard por ruta:
{ path: "/admin", component: Admin,
  beforeEnter: (to) => { if (!isAdmin()) return "/"; }
}

children define rutas anidadas (renderizadas en el <RouterView> del padre). beforeEach es guard global (auth, permisos). beforeEnter por ruta específica. meta para datos custom (requiresAuth). Retornar path para redireccionar; false para cancelar. Guards: beforeEachbeforeEnterbeforeRouteEnter (componente).

Vue DevTools
// Extensión de navegador: "Vue.js DevTools"
// (Chrome, Firefox, Edge)

// Funcionalidades:
// 1. Components: árbol de componentes + estado
//    - Inspeccionar props, data, computed
//    - Editar valores en tiempo real
//    - Ver slots y attrs

// 2. Pinia: stores + estado + historial
//    - Time-travel (volver a estados anteriores)
//    - Ver mutaciones en tiempo real

// 3. Router: rutas, params, guards
//    - Historial de navegación

// 4. Performance: timeline de renders
//    - Identificar re-renders innecesarios

// 5. Timeline: eventos, fetch, logs

// Activar en producción:
// app.config.devtools = true; (NO recomendado)

Vue DevTools es la extensión esencial para debug. Inspeccionar árbol de componentes, props, estado reactivo. Pinia tab con time-travel debugging. Performance tab identifica renders lentos. Editar estado en tiempo real para probar escenarios. Solo funciona en development (por defecto). Indispensable para el desarrollo Vue.

Patrones y buenas prácticas
// 1. Componentes pequeños y enfocados
//    Máx ~200 líneas; extraer a hijos

// 2. Props down, events up
//    Los datos fluyen del padre; los eventos suben

// 3. Composables para lógica reutilizable
//    useAuth(), useFetch(), usePagination()

// 4. Nombres descriptivos (multi-word)
//    AppHeader, UserCard, BaseButton

// 5. TypeScript siempre que sea posible
//    defineProps<T>(), defineEmits<T>()

// 6. Estado mínimo en el componente
//    Global → Pinia; Local → ref/reactive

// 7. computed > métodos en el template
//    Caché automática

// 8. key estable en v-for (nunca el índice)

// 9. Lazy load de rutas y componentes pesados

// 10. Testear composables y componentes críticos

Buenas prácticas Vue: componentes pequeños y enfocados. Props down, events up (flujo unidireccional). Composables para lógica compartida. TypeScript para seguridad de tipos. Estado mínimo local; global en Pinia. computed con caché en vez de métodos. :key estable (nunca el índice). Lazy loading para performance. Tests para lógica crítica.

Pinia (crear store)
// stores/counter.js:
import { defineStore } from "pinia";
import { ref, computed } from "vue";

// Setup store (recomendado):
export const useCounterStore = defineStore("counter", () => {
  const n = ref(0);
  const double = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  async function fetchInitial() {
    n.value = await api.getCounter();
  }

  return { n, double, inc, dec, fetchInitial };
});

// Uso:
<script setup>
import { useCounterStore } from "@/stores/counter";
const store = useCounterStore();
</script>
<template>
  <p>{{ store.n }} (doble: {{ store.double }})</p>
  <button @click="store.inc()">+1</button>
</template>

Pinia es el gestor de estado oficial de Vue (sustituye a Vuex). defineStore() con setup function (Composition API). Retorna refs, computed y métodos. Acceso directo: store.n, store.inc(). Estado compartido entre componentes. Reactivo — actualiza templates automáticamente. Los setup stores son más flexibles que los option stores.

Vitest (tests unitarios)
// Componente: Counter.vue
// Test: Counter.spec.js
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "./Counter.vue";

describe("Counter", () => {
  it("muestra 0 inicialmente", () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain("0");
  });

  it("incrementa al hacer clic", async () => {
    const wrapper = mount(Counter);
    await wrapper.find("button").trigger("click");
    expect(wrapper.text()).toContain("1");
  });

  it("recibe props", () => {
    const wrapper = mount(Counter, {
      props: { initial: 5 }
    });
    expect(wrapper.text()).toContain("5");
  });
});

// Ejecutar: npx vitest

Vitest es el test runner oficial (rápido, compatible con Jest). @vue/test-utils para montar componentes. mount() renderiza el componente. find() + trigger() para interactuar. await después de trigger (espera a que el DOM se actualice). Testear comportamiento, no implementación. Para composables: testear directamente (sin mount).