Hasta ahora has visto la misma pantalla de Nómada Tareas escrita en JavaScript puro y en React, y las dos versiones compartían un modelo: una función que se vuelve a ejecutar entera y un motor que compara el resultado con el anterior. Vue cambia eso de raíz. Su reactividad es de grano fino: no compara árboles, sino que registra qué expresión lee qué dato y actualiza solo lo que depende de lo que ha cambiado. Es la familia 2 de 10-01, en su versión más pulida y accesible. Y cambia también la unidad de trabajo: en lugar de repartir una tarjeta entre un <template>, un fichero CSS y dos módulos JavaScript, Vue la agrupa entera en un Componente de un Solo Fichero, con su plantilla, su lógica y sus estilos con ámbito propio. En esta lección verás por qué se le llama framework progresivo y cómo se añade tanto a una página existente como a un proyecto completo; la anatomía de un componente con <template>, <script setup> y <style scoped>; toda la sintaxis de plantilla —interpolación, v-bind, v-on, v-if frente a v-show, v-for con :key (el mismo data-id de siempre) y v-model explicado como azúcar y no como magia—; la reactividad de verdad, con ref, reactive, por qué existe .value, cómo los proxies detectan las dependencias y dónde se pierde la reactividad —el error que más desconcierta—; computed comparado con tu caché por versión de 09-02 y con useMemo; watch y watchEffect con el criterio para elegir; el ciclo de vida con onMounted y onUnmounted, que vuelve a ser tu destruir(); la comunicación con props, emits, slots y provide/inject; los composables como equivalente de los hooks personalizados; Pinia en su versión mínima; y el ecosistema oficial. Al final, la lista de tareas con su filtro reimplementada en Vue y comparada con las dos versiones anteriores.
Contenido
- Qué significa «framework progresivo»
- Añadir Vue a una página existente
- Un proyecto completo con Vite
- Componentes de un Solo Fichero
- Por qué agrupar plantilla, lógica y estilos
- La plantilla declarativa: interpolación
v-bind: atributos dinámicosv-on: eventosv-if,v-elseyv-showv-fory:keyv-model: la doble vinculación explicada como azúcar- Reactividad:
refy por qué existe.value reactivey cuándo usar cada uno- Cómo detectan las dependencias los proxies
- Los límites de la reactividad: dónde se pierde
computed: derivados con cachéwatchywatchEffect- Cuándo usar
computed,watchowatchEffect - El ciclo de vida:
onMountedyonUnmounted - Comunicación:
propsyemits slots: composición de contenidoprovideeinject- Composables:
usarTableroyusarFiltroResponsable - Estado global con Pinia
- El ecosistema oficial y la fragmentación
- Nómada Tareas en Vue: la lista completa
- Comparación con React y con JavaScript puro
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué significa «framework progresivo»
Vue se define a sí mismo como un framework progresivo, y esa etiqueta tiene un significado técnico concreto: se puede adoptar por capas, empezando por la más pequeña, sin comprometerse con las demás.
| Nivel de adopción | Qué usas | Qué necesitas |
|---|---|---|
| 1 · Mejorar una página existente | Vue desde una etiqueta <script>, controlando un <div> |
Nada: ni compilación, ni npm |
| 2 · Componentes en varias páginas | Vue + Componentes de un Solo Fichero | Vite |
| 3 · Aplicación de una sola página | + Vue Router | Vite |
| 4 · Aplicación grande | + Pinia, pruebas, TypeScript | La cadena completa |
| 5 · Renderizado en servidor | + Nuxt | El meta-framework |
Esa gradación no existe en Angular, que exige toda la plataforma desde el principio, y en React solo existe a medias (necesitas compilación para JSX desde el primer minuto, salvo que renuncies a él). Es la razón por la que Vue aparece tanto en proyectos que empezaron sin framework y crecieron: se pueden convertir por trozos.
Para Nómada Tareas eso significa algo muy concreto. Podrías coger la aplicación actual —tu index.html, tu Tablero, tus utilidades— y convertir solo la lista de tareas en un componente Vue, dejando el resto intacto. No es un ejercicio teórico: es una estrategia de migración real, y la retomarás en 10-06.
- Añadir Vue a una página existente
El nivel 1, sin ninguna herramienta:
<div id="tablero">
<label>
Responsable:
<select v-model="responsable">
<option :value="null">Todos</option>
<option v-for="n in responsables" :key="n" :value="n">{{ n }}</option>
</select>
</label>
<p>{{ visibles.length }} de {{ tareas.length }} tareas · {{ horasAbiertas }} h abiertas</p>
<ul>
<li v-for="tarea in visibles" :key="tarea.id">
{{ tarea.titulo }} — {{ tarea.responsable }} ({{ tarea.horasEstimadas }} h)
</li>
</ul>
</div>
<script type="module">
import { createApp, ref, computed } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { BACKLOG } from './js/datos/backlog.js';
createApp({
setup() {
const tareas = ref(BACKLOG);
const responsable = ref(null);
const responsables = computed(() =>
[...new Set(tareas.value.map((t) => t.responsable).filter(Boolean))].sort()
);
const visibles = computed(() =>
responsable.value === null
? tareas.value
: tareas.value.filter((t) => t.responsable === responsable.value)
);
const horasAbiertas = computed(() =>
visibles.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);
return { tareas, responsable, responsables, visibles, horasAbiertas };
}
}).mount('#tablero');
</script>Merece la pena detenerse aquí, porque este bloque ya contiene casi todos los conceptos de la lección y funciona abriendo el HTML en el navegador, sin npm install, sin compilar y sin configurar nada. Fíjate en tres cosas:
- La plantilla es HTML válido.
v-for,:valuey{{ }}son atributos y texto; el navegador los ignora hasta que Vue los interpreta. Esto tiene una consecuencia práctica importante: el marcado se puede escribir en cualquier editor de HTML, y una persona de diseño lo entiende. - Vue solo controla
#tablero. El resto de la página sigue siendo tuyo. Podrías tener tres islas Vue independientes en la misma página junto a tu JavaScript de siempre. BACKLOGes tu módulo de datos, sin adaptar. Vue no tiene opinión sobre de dónde salen los objetos.
Para producción se usaría una versión compilada en lugar de la que interpreta plantillas en el navegador, pero el modelo mental es este.
- Un proyecto completo con Vite
Para el resto de la lección usaremos el nivel 2 en adelante:
El asistente pregunta si quieres TypeScript, Vue Router, Pinia, pruebas con Vitest y Playwright o Cypress. Esa lista es reveladora: todo el ecosistema esencial es oficial, mantenido por el mismo equipo y con documentación integrada. Es lo contrario de la tabla de decisiones de 10-02.
El punto de entrada:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import './estilos.css';
createApp(App).mount('#raiz');
- Componentes de un Solo Fichero
El fichero .vue es la unidad de trabajo de Vue: plantilla, lógica y estilos en el mismo sitio.
<!-- src/componentes/TarjetaTarea.vue -->
<script setup>
import { computed } from 'vue';
import { SIGUIENTE, ETIQUETA, estaVencida } from '../dominio/reglas.js';
const props = defineProps({
tarea: { type: Object, required: true }
});
const emit = defineEmits(['avanzar']);
const vencida = computed(() => estaVencida(props.tarea));
const siguiente = computed(() => SIGUIENTE[props.tarea.estado]);
</script>
<template>
<li
class="tarea"
:class="[
`tarea--${tarea.prioridad}`,
{ 'tarea--hecha': tarea.estado === 'hecha', 'tarea--vencida': vencida }
]"
:data-id="tarea.id"
>
<h3 class="tarea__titulo">{{ tarea.titulo }}</h3>
<p class="tarea__meta">
{{ tarea.responsable ?? 'sin asignar' }} · {{ tarea.horasEstimadas }} h
<span v-if="vencida" class="tarea__aviso"> · ⚠ vencida</span>
</p>
<ul class="tarea__etiquetas">
<li v-for="etiqueta in tarea.etiquetas" :key="etiqueta" class="etiqueta">
{{ etiqueta }}
</li>
</ul>
<button
type="button"
:disabled="siguiente === null"
:aria-label="`${ETIQUETA[tarea.estado]}: ${tarea.titulo}`"
@click="emit('avanzar', tarea.id)"
>
{{ ETIQUETA[tarea.estado] }}
</button>
</li>
</template>
<style scoped>
.tarea { border-left: 4px solid var(--borde); padding: 0.75rem; }
.tarea--alta { border-left-color: var(--rojo); }
.tarea--hecha .tarea__titulo { text-decoration: line-through; opacity: 0.6; }
.tarea--vencida { background: var(--aviso-suave); }
.tarea__etiquetas { list-style: none; display: flex; gap: 0.3rem; padding: 0; }
</style>Tres bloques, con reglas propias:
<script setup>es azúcar de compilación. Todo lo que declares en el nivel superior —variables, funciones, importaciones— queda disponible en la plantilla automáticamente, sinreturn. Es la forma actual y recomendada de escribir componentes Vue; verás código antiguo conexport default { setup() { … return { … } } }o condata()/methods(la API de opciones), que siguen funcionando pero ya no son la primera opción.<template>contiene HTML con directivas. Se compila a una función de renderizado durante la construcción.<style scoped>aplica los estilos solo a este componente. El compilador añade un atributo único a los elementos del componente y otro a los selectores, de modo que.tareade aquí no puede afectar a un.tareade otro sitio.
- Por qué agrupar plantilla, lógica y estilos
Ese scoped merece un párrafo propio, porque es donde Vue resuelve el problema 7 de 10-01 de forma más directa que React.
Recuerda tu tarjeta en Nómada Tareas: el <template id="plantilla-tarea"> en index.html, las clases .tarea__* en css/estilos.css, la lógica en js/vista/tarjeta.js y el comportamiento en js/vista/controlador.js. Cuatro ficheros, cuatro acoplamientos y ninguno comprobado. Renombrar .tarea__titulo en el CSS no produce ningún error: simplemente la tarjeta se ve mal.
Con un fichero .vue, los cuatro están a la vista en la misma pantalla. Cambiar el nombre de una clase es una operación de buscar y reemplazar dentro de un fichero de 40 líneas. Y como los estilos tienen ámbito, no hay que inventar convenciones de nombres (BEM y similares) para evitar colisiones: el aislamiento es real, no una disciplina.
La objeción clásica —«mezclar HTML, CSS y JavaScript es mala práctica»— confunde dos cosas. La separación de intereses no es separación de tecnologías: es separación de responsabilidades. La estructura, el aspecto y el comportamiento de una tarjeta son un solo interés, y repartirlos por tres ficheros no los desacopla, solo los aleja. Es exactamente el argumento del apartado 12 de 10-01 sobre el componente como unidad correcta de reutilización.
- La plantilla declarativa: interpolación
El texto dinámico se escribe con dobles llaves:
<h3>{{ tarea.titulo }}</h3>
<p>{{ tarea.horasEstimadas }} h · {{ tarea.horasEstimadas * PESOS[tarea.prioridad] }} de esfuerzo</p>
<p>{{ tarea.responsable ?? 'sin asignar' }}</p>Dentro va una expresión JavaScript, no una sentencia — la misma regla que las llaves de JSX. Y el contenido se escapa siempre: es textContent, no innerHTML, con la misma protección de 06-02.
Diferencia con React que conviene fijar: en JSX escribes {} para todo lo dinámico, incluidos los atributos. En Vue, las llaves son solo para texto; los atributos usan v-bind.
v-bind: atributos dinámicos
v-bind: atributos dinámicos<!-- Forma larga y forma abreviada, idénticas -->
<li v-bind:data-id="tarea.id">…</li>
<li :data-id="tarea.id">…</li>
<button :disabled="siguiente === null">…</button>
<img :src="tarea.imagen" :alt="`Foto de ${tarea.titulo}`">Dos casos especiales que se usan constantemente:
Clases. :class acepta una cadena, un objeto (clave = clase, valor = condición) o un array, y se combina con el atributo class estático:
<li
class="tarea"
:class="[
`tarea--${tarea.prioridad}`,
{ 'tarea--hecha': tarea.estado === 'hecha', 'tarea--vencida': vencida }
]"
>Compara con lo que hacías en pintarTarjeta:
li.classList.remove(...CLASES_PRIORIDAD);
li.classList.add(`tarea--${tarea.prioridad}`);
li.classList.toggle('tarea--hecha', tarea.estado === 'hecha');
li.classList.toggle('tarea--vencida', vencida);Cuatro instrucciones imperativas que hay que mantener sincronizadas —incluido el remove previo, que es fácil olvidar— frente a una descripción declarativa. Y frente a React, donde hay que construir la cadena a mano con plantillas o una utilidad, la sintaxis de objeto de Vue es notablemente más limpia.
Estilos. :style acepta un objeto con propiedades en camelCase:
v-on: eventos
v-on: eventos<!-- Forma larga y abreviada -->
<button v-on:click="avanzar(tarea.id)">Empezar</button>
<button @click="avanzar(tarea.id)">Empezar</button>
<!-- Con el objeto de evento -->
<select @change="alCambiar($event.target.value || null)">
<input @input="texto = $event.target.value">La diferencia visible con React es que en la plantilla puedes escribir la llamada directamente (avanzar(tarea.id)), sin envolverla en una función flecha. El compilador se encarga: @click="avanzar(tarea.id)" se convierte en un manejador que ejecuta esa expresión. Con @click="avanzar" (sin paréntesis) se pasa la función y recibe el evento como argumento.
Vue añade modificadores, que resuelven de forma declarativa cosas que en JavaScript puro escribes a mano:
| Modificador | Equivalente en JavaScript puro |
|---|---|
@submit.prevent |
evento.preventDefault() |
@click.stop |
evento.stopPropagation() |
@click.self |
if (evento.target !== evento.currentTarget) return |
@click.once |
addEventListener(…, { once: true }) |
@scroll.passive |
addEventListener(…, { passive: true }) |
@keyup.enter |
if (evento.key === 'Enter') |
@keyup.esc |
if (evento.key === 'Escape') |
Todos ellos te resultarán familiares de 06-03 y 06-07. Son atajos, no funcionalidad nueva, pero eliminan una clase entera de olvidos — sobre todo el preventDefault de los formularios.
Nota sobre delegación: como en React, escribir @click en cada uno de 600 elementos no crea 600 escuchas del DOM. El compilador genera manejadores y Vue los gestiona de forma eficiente. El patrón closest('[data-accion]') de 06-04 sigue siendo válido si lo necesitas, pero ya no es obligatorio.
v-if, v-else y v-show
v-if, v-else y v-show<p v-if="visibles.length === 0" class="lista-vacia">
Ninguna tarea coincide con el filtro.
</p>
<ul v-else class="lista-tareas">
<li v-for="tarea in visibles" :key="tarea.id">…</li>
</ul>Y la comparación que hay que tener clara:
v-if |
v-show |
|
|---|---|---|
| Qué hace | Crea o destruye el elemento del DOM | Lo crea siempre y alterna display: none |
| Coste al alternar | Alto: montar y desmontar | Muy bajo: una propiedad CSS |
| Coste inicial si es falso | Cero: no se crea nada | Se crea igualmente |
| Ciclo de vida del componente | Se dispara onMounted/onUnmounted |
No se dispara nada |
| Nodos en el DOM | Solo los visibles | Todos, siempre |
| Encontrable con Ctrl+F | No | Sí (está en el DOM, oculto) |
| Cuándo usarlo | Condición que cambia poco, o contenido caro | Alternancia muy frecuente de algo ligero |
Esta tabla es la formalización de algo que ya mediste en 09-04: ocultar 594 tareas con hidden mantenía 7.812 nodos en el DOM, mientras que eliminarlas dejaba 1.194. v-if es lo segundo, v-show lo primero. Para el filtro de responsable, v-if es lo correcto.
Vue permite además agrupar sin añadir un nodo:
<template v-if="cargando">
<p role="status">Cargando tareas…</p>
<div class="esqueleto" aria-hidden="true"></div>
</template>Es el equivalente del fragmento <>…</> de React.
v-for y :key
v-for y :key<ul class="lista-tareas">
<li v-for="tarea in visibles" :key="tarea.id" class="tarea">
{{ tarea.titulo }}
</li>
</ul>
<!-- Con índice -->
<li v-for="(tarea, indice) in visibles" :key="tarea.id">
{{ indice + 1 }}. {{ tarea.titulo }}
</li>
<!-- Sobre un objeto -->
<li v-for="(cantidad, estado) in resumenPorEstado" :key="estado">
{{ estado }}: {{ cantidad }}
</li>Y aquí está, por tercera vez en el módulo, el mismo concepto: :key es tu data-id de reconciliar (06-06) y la key de React (10-02). Vue lo usa exactamente igual: construye un mapa de clave a nodo, empareja por clave y no por posición, reutiliza los que siguen vivos y elimina los sobrantes.
Las mismas tres reglas: estable, única entre hermanos, nunca el índice si la lista se mueve. Y una ventaja pequeña pero útil de Vue: la regla de ESLint del complemento oficial marca como error un v-for sin :key, mientras que en React es solo un aviso en la consola. Es una decisión de framework «con más opiniones», en el sentido de 10-01.
Un detalle que Vue hace mejor que el DOM virtual puro: como el compilador analiza la plantilla, sabe qué partes son estáticas y las salta por completo en las actualizaciones. Si tu <li> tiene diez elementos y solo uno depende de datos, Vue actualiza uno. React ejecuta la función entera y compara los diez. Es la mezcla de familias 2 y 3 de 10-01 en acción.
v-model: la doble vinculación explicada como azúcar
v-model: la doble vinculación explicada como azúcarv-model es la característica más famosa de Vue y la peor entendida. Se presenta como «doble vinculación» y suena a magia bidireccional. No lo es: es azúcar sintáctico de dos cosas que ya sabes hacer.
se compila, esencialmente, en:
Es decir: un v-bind que baja el valor y un v-on que lo sube. Exactamente el formulario controlado de 10-02, escrito en una línea en lugar de dos. No hay canal mágico ni observación del DOM: hay un atributo y un evento.
Vue adapta la pareja según el elemento, que es lo que ahorra trabajo de verdad:
| Elemento | Propiedad que enlaza | Evento que escucha |
|---|---|---|
<input type="text"> |
value |
input |
<textarea> |
value |
input |
<input type="checkbox"> |
checked |
change |
<input type="radio"> |
checked |
change |
<select> |
value |
change |
<select multiple> |
array de valores | change |
Con modificadores útiles:
<input v-model.trim="titulo"> <!-- aplica .trim(): útil para la R2 -->
<input v-model.number="horas"> <!-- convierte a número: sin él, "6" es cadena -->
<input v-model.lazy="busqueda"> <!-- escucha `change` en vez de `input` -->v-model.number merece una nota, porque conecta con 01-07: sin él, <input type="number"> devuelve una cadena, y horas > 40 compararía texto. Es uno de los errores más frecuentes al validar la R3.
En componentes propios, v-model funciona sobre una prop y un evento:
<!-- Componente hijo: FiltroResponsable.vue -->
<script setup>
defineProps({ modelValue: { type: String, default: null } });
defineEmits(['update:modelValue']);
</script>
<template>
<select :value="modelValue" @change="$emit('update:modelValue', $event.target.value || null)">
<option value="">Todos</option>
<slot />
</select>
</template>Otra vez: una prop que baja y un evento que sube. El flujo de datos sigue siendo unidireccional; v-model solo pone nombre al patrón. Entenderlo así evita el malentendido de creer que el hijo modifica el estado del padre — no lo hace: le pide que lo modifique.
- Reactividad:
ref y por qué existe .value
ref y por qué existe .valueLlegamos al corazón de Vue.
import { ref } from 'vue';
const responsable = ref(null);
console.log(responsable.value); // null
responsable.value = 'Iván'; // ← esto dispara la actualización de todo lo que dependaLa pregunta que todo el mundo hace al principio: ¿por qué .value?
La respuesta está en una limitación del lenguaje que conoces desde 01-05: JavaScript no permite interceptar la asignación a una variable. No hay forma de que responsable = 'Iván' ejecute código. Se puede interceptar el acceso a una propiedad de un objeto —eso es lo que hacen los getter/setter de 05-03 y los Proxy—, pero no a una variable suelta.
Así que Vue hace lo único posible: ref(null) devuelve un objeto con una única propiedad, value, cuyo get y set están interceptados.
// Lo que es un ref, conceptualmente (05-03: getters y setters)
function ref(inicial) {
let interno = inicial;
const suscriptores = new Set();
return {
get value() {
registrarDependencia(suscriptores); // ← quien lee, se apunta
return interno;
},
set value(nuevo) {
if (Object.is(interno, nuevo)) return; // sin cambio, sin trabajo
interno = nuevo;
notificar(suscriptores); // ← quien leyó, se entera
}
};
}Ese .value que parece una molestia es, literalmente, el precio de que exista el mecanismo. Y hay una compensación importante: en la plantilla se desenvuelve solo. Dentro de <template> escribes {{ responsable }}, no {{ responsable.value }}, porque el compilador sabe qué variables son refs y añade el .value por ti.
Comparación directa con lo que ya conoces:
useState de React |
ref de Vue |
|
|---|---|---|
| Leer | responsable |
responsable.value (en plantilla: responsable) |
| Escribir | setResponsable('Iván') |
responsable.value = 'Iván' |
| Qué provoca | Reejecutar el componente entero | Actualizar solo lo que lee ese ref |
| Se puede escribir desde fuera del componente | No | Sí: un ref es un objeto normal |
| Comparación de cambio | Object.is sobre el valor |
Object.is sobre el valor |
La tercera fila es la diferencia fundamental. Cambiar responsable.value no reejecuta App: reejecuta las expresiones concretas que leyeron ese ref. Ese es el grano fino de 10-01.
Y la cuarta tiene una consecuencia práctica muy útil: un ref se puede crear en un módulo, exportar y modificar desde cualquier sitio —incluido tu CanalTablero de 07-04 al recibir un mensaje del WebSocket— sin estar dentro de ningún componente.
reactive y cuándo usar cada uno
reactive y cuándo usar cada unoreactive es la alternativa para objetos: envuelve el objeto entero en un Proxy y no necesita .value.
import { reactive } from 'vue';
const filtros = reactive({ responsable: null, texto: '', orden: 'prioridad' });
filtros.responsable = 'Iván'; // sin .value: reactivo directamente
filtros.texto = 'serigrafía';Es más cómodo, pero tiene cuatro limitaciones reales:
| Limitación | Consecuencia |
|---|---|
Solo funciona con objetos, arrays, Map y Set |
Un número o una cadena no pueden ser reactive |
| No se puede reemplazar el objeto entero | filtros = {…} rompe la reactividad; hay que asignar propiedad a propiedad |
| Se pierde al desestructurar | const { responsable } = filtros copia el valor, no el vínculo |
| Se pierde al pasar una propiedad primitiva a una función | Se pasa el valor, no la referencia |
La recomendación actual, y la que sigue casi todo el código Vue moderno: usa ref por defecto. Funciona con cualquier tipo, se puede reemplazar entero, y el .value explícito hace visible dónde está la reactividad. reactive queda para objetos de estado agrupado cuyas propiedades se manipulan una a una, y aun así muchos equipos prefieren un ref con un objeto dentro:
const filtros = ref({ responsable: null, texto: '' });
filtros.value.responsable = 'Iván'; // reactivo
filtros.value = { responsable: null, texto: '' }; // reemplazo completo, también reactivo
- Cómo detectan las dependencias los proxies
Toca explicar el mecanismo de verdad, porque de él salen todos los comportamientos —los buenos y los raros.
Vue mantiene, mientras ejecuta un efecto reactivo (el render de un componente, un computed, un watchEffect), una referencia al efecto actual. Cuando durante esa ejecución se lee una propiedad reactiva, el Proxy intercepta la lectura y registra: «este efecto depende de esta propiedad de este objeto». Cuando alguien escribe esa propiedad, el proxy busca la lista de efectos dependientes y los vuelve a ejecutar.
graph TD E["Efecto reactivo<br/>(render, computed, watchEffect)"] --> L["Lee tarea.estado"] L --> P["Proxy intercepta el get"] P --> R["Registra: efecto depende de<br/>(objetoTarea, 'estado')"] W["Alguien escribe tarea.estado = 'hecha'"] --> P2["Proxy intercepta el set"] P2 --> B["Busca los efectos dependientes<br/>de (objetoTarea, 'estado')"] B --> E2["Los vuelve a ejecutar"]
Tres consecuencias que explican todo lo demás:
1 · La detección es automática y precisa. No declaras dependencias como en el array de useEffect: se descubren al ejecutar. Eso elimina de un plumazo la clase entera de errores «falta una dependencia» y «esta dependencia cambia siempre» de 10-02.
2 · La detección es dinámica. Si un computed tiene un if, las dependencias de la rama no tomada no se registran en esta ejecución. Es correcto y es lo que quieres: si el resultado no depende de un dato, cambiar ese dato no debe recalcular nada.
3 · La detección solo funciona durante la ejecución del efecto. Si lees un valor reactivo dentro de un setTimeout, un .then() o un addEventListener, esa lectura ocurre después, cuando el efecto ya terminó y el registro está cerrado. No se registra nada. Esta es la causa de la mitad de los «esto no se actualiza» de Vue.
// ❌ La lectura ocurre fuera del contexto de seguimiento
watchEffect(() => {
setTimeout(() => {
console.log(responsable.value); // no registra dependencia
}, 100);
});
// ✅ La lectura ocurre dentro
watchEffect(() => {
const actual = responsable.value; // ← se registra aquí
setTimeout(() => console.log(actual), 100);
});
- Los límites de la reactividad: dónde se pierde
Este apartado es el que separa a quien sabe Vue de quien lo ha probado. La reactividad se pierde en cuatro situaciones, y todas tienen la misma causa: el vínculo vive en la propiedad, no en el valor.
Límite 1 · Desestructurar un objeto reactivo.
const filtros = reactive({ responsable: null, texto: '' });
// ❌ `responsable` es una copia del valor, sin vínculo
const { responsable } = filtros;
console.log(responsable); // null, y seguirá siendo null para siempre
// ✅ toRefs mantiene el vínculo convirtiendo cada propiedad en un ref
import { toRefs } from 'vue';
const { responsable: refResponsable } = toRefs(filtros);
console.log(refResponsable.value); // sigue vinculadotoRefs recorre el objeto y crea, para cada propiedad, un ref cuyo get/set apuntan a la propiedad original. Es la herramienta específica para desestructurar sin romper nada, y se usa mucho al devolver el estado desde un composable.
Límite 2 · Desestructurar las props.
const props = defineProps({ tarea: Object });
const { tarea } = props; // ❌ pierde la reactividad
// ✅ usar props.tarea directamente, o toRefs(props)Con un matiz importante: <script setup> tiene una transformación del compilador que hace reactiva la desestructuración de props escrita directamente en defineProps. Fuera de ese caso concreto, la regla se mantiene.
Límite 3 · Pasar un primitivo a una función.
const contador = ref(0);
function incrementar(valor) { valor++; } // ❌ recibe una copia del número
incrementar(contador.value); // no hace nada
function incrementarRef(ref) { ref.value++; } // ✅ recibe el objeto ref
incrementarRef(contador);Este es el motivo profundo por el que ref es un objeto: los objetos se pasan por referencia y los primitivos por valor, exactamente como aprendiste en 01-05 y 04-08. ref convierte un primitivo en algo que se puede pasar sin perderlo.
Límite 4 · Reemplazar un objeto reactive entero.
let filtros = reactive({ responsable: null });
filtros = reactive({ responsable: 'Iván' }); // ❌ la plantilla sigue apuntando al primeroLa comparación honesta con React, que es la contrapartida anunciada en 10-01:
| React | Vue | |
|---|---|---|
| Declarar dependencias | A mano, en un array | Automático, al leer |
| Error típico | Dependencia que falta u objeto que cambia siempre | Reactividad perdida al desestructurar o al leer fuera del efecto |
| Cuándo se detecta el error | ESLint avisa | En ejecución, sin aviso: «no se actualiza» |
| Modelo mental | Simple, verboso | Sutil, conciso |
Ninguno de los dos es «más fácil». React te obliga a declarar y por eso puede comprobarlo con una regla de ESLint; Vue lo hace solo y por eso el fallo es silencioso. Es un intercambio real.
computed: derivados con caché
computed: derivados con cachécomputed declara un valor derivado de otros valores reactivos:
import { ref, computed } from 'vue';
const tareas = ref(BACKLOG);
const responsable = ref(null);
const visibles = computed(() =>
responsable.value === null
? tareas.value
: tareas.value.filter((t) => t.responsable === responsable.value)
);
const horasAbiertas = computed(() =>
visibles.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);Cuatro propiedades:
- Se cachea. El cálculo solo se ejecuta si alguna dependencia cambió. Leer
visibles.valuecien veces sin cambios ejecuta el filtro una vez. - Se compone.
horasAbiertasdepende devisibles, que depende detareasyresponsable. Vue construye el grafo y propaga en el orden correcto, sin recalcular nada dos veces. - Es perezoso. Si nadie lee
horasAbiertas, no se calcula, aunque cambietareas. - Es de solo lectura por defecto (se puede definir un
set, pero es raro y casi siempre indica un diseño mejorable).
Y aquí está el paralelismo que cierra 09-02:
// Tu caché por versión de 09-02
resumen(hoy = HOY) {
if (this.#cacheResumen?.version === this.#version && this.#cacheResumen.hoy === hoy) {
return this.#cacheResumen.valor;
}
const valor = this.#calcularResumen(hoy);
this.#cacheResumen = { version: this.#version, hoy, valor };
return valor;
}Escribiste veinte líneas —contador de versión, invalidación en cada mutación, comparación de la clave de caché— y el riesgo permanente de olvidar un #invalidar(). computed hace lo mismo, con la invalidación descubierta automáticamente y sin posibilidad de olvido.
Comparación con useMemo de React, que es el equivalente más cercano:
useMemo (React) |
computed (Vue) |
|
|---|---|---|
| Dependencias | Declaradas a mano | Detectadas automáticamente |
| Riesgo de error | Olvidar una dependencia | Leer fuera del contexto de seguimiento |
| Cuándo se usa | Como optimización, cuando mides que duele | Como forma natural de escribir derivados |
| Se puede usar fuera de un componente | No | Sí |
| Coste si sobra | Comparar el array de dependencias | Mínimo |
La tercera fila es la diferencia cultural entre los dos frameworks. En React, useMemo es la excepción y calcular directamente es la norma. En Vue, computed es la norma: cualquier derivado se declara así, sin pensar en rendimiento, porque es la forma correcta de expresarlo. Y por eso en Vue casi nunca se ve el debate sobre memoización excesiva de 10-02.
watch y watchEffect
watch y watchEffectLos dos hacen efectos secundarios cuando algo cambia. Son el equivalente de useEffect, con la misma advertencia: no son para derivar valores —para eso está computed.
watch observa una fuente concreta y recibe el valor nuevo y el anterior:
import { watch } from 'vue';
watch(responsable, (nuevo, anterior) => {
console.log(`Filtro: ${anterior} → ${nuevo}`);
actualizarUrl({ responsable: nuevo }); // History API, 07-06
});
// Varias fuentes
watch([responsable, texto], ([r, t]) => guardarPreferencias({ r, t }));
// Un getter, para observar una propiedad concreta
watch(() => props.tarea.estado, (nuevo) => {
if (nuevo === 'hecha') anunciar(`${props.tarea.titulo} completada`);
});
// Con opciones
watch(responsable, cargarTareas, { immediate: true }); // se ejecuta también al inicio
watch(filtros, guardar, { deep: true }); // observa cambios anidadoswatchEffect ejecuta la función inmediatamente y descubre solas sus dependencias, como un computed pero para efectos:
import { watchEffect } from 'vue';
watchEffect(() => {
document.title = `Nómada Tareas (${pendientes.value})`; // depende de `pendientes`
});Y ambos aceptan una limpieza, que es otra vez tu destruir():
watchEffect((alLimpiar) => {
const controlador = new AbortController();
listarTareas({ responsable: responsable.value, signal: controlador.signal })
.then((datos) => { tareas.value = datos; })
.catch((error) => { if (error.name !== 'AbortError') fallo.value = error; });
alLimpiar(() => controlador.abort()); // se ejecuta antes de la siguiente y al desmontar
});Ese bloque resuelve la condición de carrera de 10-02 con el mismo AbortController de 07-03, y en Vue no hay que declarar [responsable] en ningún array: el watchEffect se apuntó como dependiente al leer responsable.value.
- Cuándo usar
computed, watch o watchEffect
computed, watch o watchEffectLa tabla que evita el 90 % de los errores de reactividad en Vue:
| Necesitas… | Usa | Por qué |
|---|---|---|
| Un valor derivado de otros | computed |
Cacheado, perezoso, sin efectos |
| Reaccionar a un cambio concreto sabiendo el valor anterior | watch |
Da nuevo y anterior, y es explícito |
| Sincronizar con algo externo usando varias fuentes | watchEffect |
Detecta las dependencias solo |
| Ejecutar algo solo al montar | onMounted |
Es un momento del ciclo, no un cambio |
Guardar un valor derivado en un ref con watch |
Nada: usa computed |
Es el antipatrón equivalente al de 10-02 |
Ese último caso merece verse escrito, porque es exactamente el error del apartado 16 de 10-02 traducido a Vue:
// ❌ MAL: un watch para derivar
const horasAbiertas = ref(0);
watch(tareas, (t) => {
horasAbiertas.value = t.filter((x) => x.estado !== 'hecha')
.reduce((s, x) => s + x.horasEstimadas, 0);
}, { immediate: true });
// ✅ BIEN
const horasAbiertas = computed(() =>
tareas.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);Los mismos tres defectos: dos fuentes de verdad, un momento en que el valor está desfasado, y más código. La regla es transferible entre frameworks: si es un valor, decláralo como derivado; si es una acción, hazla en un efecto.
- El ciclo de vida:
onMounted y onUnmounted
onMounted y onUnmountedimport { onMounted, onUnmounted, onUpdated, ref } from 'vue';
const contenedor = ref(null); // un ref de plantilla: apuntará al nodo real
onMounted(() => {
// El DOM ya existe: aquí se puede medir, enfocar, observar
const observador = new IntersectionObserver(alEntrar); // 07-06
observador.observe(contenedor.value);
onUnmounted(() => observador.disconnect()); // ← la limpieza
});Los momentos principales, con su equivalencia:
| Vue | React | Tu Nómada Tareas |
|---|---|---|
onMounted |
useEffect(…, []) |
El constructor de TableroVista + primer render() |
onUpdated |
Ejecución del componente | actualizar() |
onUnmounted |
return de useEffect |
destruir() (09-03) |
onErrorCaptured |
Límite de error | El try/catch de 02-05 en el controlador |
La tercera fila vuelve a ser la importante, y el mensaje de 10-01 sigue vigente: Vue te da el momento garantizado, no adivina qué apagar. Un setInterval sin clearInterval en onUnmounted es exactamente la misma fuga que en JavaScript puro.
Detalle práctico: un ref de plantilla (ref="contenedor" en el HTML más const contenedor = ref(null) en el script) es el equivalente de useRef de React y de tu $('.lista-tareas') de siempre. Vale null hasta que el componente se monta, así que solo se puede usar dentro de onMounted o después.
- Comunicación:
props y emits
props y emitsEl flujo es el mismo que en React —datos hacia abajo, eventos hacia arriba— con una diferencia: en Vue se declara explícitamente en las dos direcciones.
<script setup>
// Hacia abajo: props, con tipo, obligatoriedad y validación
const props = defineProps({
tarea: {
type: Object,
required: true,
validator: (t) => typeof t.id === 'number' && typeof t.titulo === 'string'
},
compacta: { type: Boolean, default: false }
});
// Hacia arriba: eventos declarados, con validación opcional
const emit = defineEmits({
avanzar: (id) => typeof id === 'number',
eliminar: (id) => typeof id === 'number'
});
function alPulsarAvanzar() {
emit('avanzar', props.tarea.id);
}
</script><!-- El padre escucha con @ -->
<TarjetaTarea :tarea="tarea" @avanzar="avanzar" @eliminar="eliminar" />Diferencias con React que merecen señalarse:
| React | Vue | |
|---|---|---|
| Datos hacia abajo | Props (sin declarar) | defineProps con tipo y validador |
| Eventos hacia arriba | Props de función (alAvanzar) |
defineEmits + emit('avanzar') |
| Validación en desarrollo | Solo con TypeScript | Incluida en JavaScript |
| Distinción datos/eventos | Ninguna: todo son props | Explícita en el API |
La declaración de emits es valiosa por una razón poco obvia: documenta el contrato completo del componente. Leyendo defineProps y defineEmits sabes todo lo que entra y todo lo que sale, sin buscar por el fichero qué funciones se llaman. En React esa información está repartida por la firma y hay que reconstruirla.
Y la misma regla que en React: las props son de solo lectura. Modificar props.tarea.estado desde el hijo es un error, y Vue avisa en desarrollo. El hijo emite; el padre decide.
slots: composición de contenido
slots: composición de contenidoLos slots son el equivalente de children de React, y son más expresivos porque admiten varios huecos con nombre.
<!-- src/componentes/Panel.vue -->
<template>
<section class="panel">
<header class="panel__cabecera">
<slot name="cabecera">
<h2>Sin título</h2> <!-- contenido por defecto si no se aporta -->
</slot>
</header>
<div class="panel__cuerpo">
<slot /> <!-- el slot por defecto -->
</div>
<footer v-if="$slots.pie" class="panel__pie">
<slot name="pie" /> <!-- solo se pinta si el padre aportó algo -->
</footer>
</section>
</template><Panel>
<template #cabecera>
<h2>Tareas abiertas · {{ horasAbiertas }} h</h2>
</template>
<ListaTareas :tareas="visibles" @avanzar="avanzar" />
<template #pie>
<button @click="limpiarFiltros">Limpiar filtros</button>
</template>
</Panel>Y los slots con ámbito, que permiten al hijo pasar datos al contenido que aporta el padre:
<!-- ListaTareas.vue: la lista controla el bucle, el padre decide cómo se ve cada fila -->
<template>
<ul class="lista-tareas">
<li v-for="tarea in tareas" :key="tarea.id">
<slot name="fila" :tarea="tarea" :vencida="estaVencida(tarea)">
{{ tarea.titulo }}
</slot>
</li>
</ul>
</template><ListaTareas :tareas="visibles">
<template #fila="{ tarea, vencida }">
<strong :class="{ rojo: vencida }">{{ tarea.titulo }}</strong>
<em>{{ tarea.responsable }}</em>
</template>
</ListaTareas>Es un patrón muy potente: el componente aporta la lógica (el bucle, las claves, el filtrado) y quien lo usa aporta la presentación. En React el equivalente se hace pasando una función como prop, y funciona igual de bien; la diferencia es de sintaxis, no de capacidad.
provide e inject
provide e injectEl equivalente de Context de React: poner un valor a disposición de todo el subárbol.
// En un ancestro
import { provide, ref, readonly } from 'vue';
const tema = ref('claro');
provide('tema', readonly(tema)); // solo lectura para los descendientes
provide('alternarTema', () => { tema.value = tema.value === 'claro' ? 'oscuro' : 'claro'; });// En cualquier descendiente, a cualquier profundidad
import { inject } from 'vue';
const tema = inject('tema', 'claro'); // con valor por defecto
const alternarTema = inject('alternarTema');Y una diferencia técnica con Context que importa: como Vue tiene reactividad de grano fino, proveer un ref no repinta a todos los que hacen inject. Solo se actualizan los que realmente leen su .value. El problema de rendimiento de Context que describía 10-03 —todos los consumidores se repintan cuando cambia el valor— sencillamente no existe aquí.
readonly es una buena práctica: los descendientes leen el valor y para cambiarlo llaman a la función provista. Así el control del estado se queda donde debe.
- Composables:
usarTablero y usarFiltroResponsable
usarTablero y usarFiltroResponsableUn composable es una función que usa el API de reactividad de Vue y devuelve estado y operaciones. Es el equivalente exacto de los hooks personalizados de 10-02, con una diferencia importante: no tiene reglas de orden de llamada, porque Vue no identifica el estado por posición sino por objeto.
// src/composables/usarTablero.js
import { ref, computed } from 'vue';
import { SIGUIENTE, PESOS } from '../dominio/reglas.js';
/** Estado del tablero de Nómada Tareas y sus operaciones. */
export function usarTablero(tareasIniciales) {
const tareas = ref(tareasIniciales);
function avanzar(id) {
const tarea = tareas.value.find((t) => t.id === id);
if (!tarea) return;
const destino = SIGUIENTE[tarea.estado];
if (destino === null) return; // R6: desde 'hecha' no se avanza
tarea.estado = destino; // mutación directa: es reactivo
}
const resumen = computed(() => {
const abiertas = tareas.value.filter((t) => t.estado !== 'hecha');
return {
total: tareas.value.length,
horasTotales: tareas.value.reduce((s, t) => s + t.horasEstimadas, 0),
horasAbiertas: abiertas.reduce((s, t) => s + t.horasEstimadas, 0),
esfuerzo: tareas.value.reduce((s, t) => s + t.horasEstimadas * PESOS[t.prioridad], 0)
};
});
return { tareas, avanzar, resumen };
}// src/composables/usarFiltroResponsable.js
import { ref, computed } from 'vue';
export function usarFiltroResponsable(tareas) {
const responsable = ref(null);
const responsables = computed(() =>
[...new Set(tareas.value.map((t) => t.responsable).filter(Boolean))].sort()
);
const visibles = computed(() =>
responsable.value === null
? tareas.value
: tareas.value.filter((t) => t.responsable === responsable.value)
);
return { responsable, responsables, visibles };
}Compara avanzar con su equivalente de React en 10-02:
// React: inmutable, obligatoriamente
setTareas((actuales) => actuales.map((t) =>
t.id === id && SIGUIENTE[t.estado] === destino ? { ...t, estado: destino } : t
));
// Vue: mutación directa
tarea.estado = destino;Esta es la diferencia práctica más visible entre los dos frameworks. En React la inmutabilidad es obligatoria porque la detección es por referencia; en Vue el proxy detecta la escritura en la propiedad, así que mutar es correcto y natural. Ninguna de las dos es mejor: React gana en previsibilidad —el estado nunca cambia bajo tus pies y las comparaciones son triviales— y Vue gana en concisión y en el hecho de que las 599 tareas que no cambian ni siquiera se tocan.
Una convención sobre nombres: el ecosistema Vue usa useAlgo en inglés, igual que React. Aquí se escriben usarTablero y usarFiltroResponsable por coherencia con el resto del curso, pero en código real verás useTablero. Lo que importa es que sea consistente.
- Estado global con Pinia
Pinia es el almacén oficial de Vue, y su versión mínima es sorprendentemente parecida a un composable:
// src/almacenes/tablero.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { SIGUIENTE } from '../dominio/reglas.js';
import { BACKLOG } from '../datos/backlog.js';
export const usarAlmacenTablero = defineStore('tablero', () => {
// ── estado ──────────────────────────────
const tareas = ref(BACKLOG);
const responsable = ref(null);
// ── derivados (equivalen a los "getters") ──
const visibles = computed(() =>
responsable.value === null
? tareas.value
: tareas.value.filter((t) => t.responsable === responsable.value)
);
const horasAbiertas = computed(() =>
visibles.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);
// ── acciones ────────────────────────────
function filtrarPor(nombre) { responsable.value = nombre; }
function avanzar(id) {
const tarea = tareas.value.find((t) => t.id === id);
if (tarea && SIGUIENTE[tarea.estado] !== null) tarea.estado = SIGUIENTE[tarea.estado];
}
return { tareas, responsable, visibles, horasAbiertas, filtrarPor, avanzar };
});<script setup>
import { storeToRefs } from 'pinia';
import { usarAlmacenTablero } from '../almacenes/tablero.js';
const almacen = usarAlmacenTablero();
const { visibles, horasAbiertas } = storeToRefs(almacen); // ← storeToRefs, no desestructurar
const { avanzar } = almacen; // las funciones sí se desestructuran
</script>storeToRefs es imprescindible y es una aplicación directa del límite 1 del apartado 15: desestructurar el almacén copiaría los valores y rompería la reactividad. storeToRefs convierte el estado y los derivados en refs vinculados; las acciones son funciones normales y se pueden desestructurar sin problema.
Comparado con Redux (10-03):
| Redux Toolkit | Pinia | |
|---|---|---|
| Conceptos | Acciones, reductores, selectores, middleware | Estado, derivados, acciones |
| Inmutabilidad | Obligatoria (con Immer para escribirla cómoda) | No hace falta: se muta |
| Trazabilidad y viaje en el tiempo | Excelente | Buena (DevTools de Vue) |
| Peso aproximado | ~13 kB | ~1,5 kB |
| Oficial del framework | No | Sí |
| Un almacén o varios | Uno, dividido en slices | Varios independientes |
La conclusión de 10-03 se mantiene: empieza sin almacén. Un composable con un ref en un módulo ya es estado compartido —porque un ref de módulo es un objeto único—, y a menudo basta. Pinia añade DevTools, sustitución en caliente, la garantía de una única instancia y soporte para renderizado en servidor. La diferencia con Redux es que el escalón es mucho más bajo: pasar de un composable a un almacén Pinia son diez minutos.
- El ecosistema oficial y la fragmentación
| Pieza | Vue (oficial) | React (a elegir) |
|---|---|---|
| Enrutado | Vue Router | React Router, TanStack Router, el del meta-framework |
| Estado global | Pinia | Redux Toolkit, Zustand, Jotai, Context |
| Empaquetado | Vite (creado por el mismo autor) | Vite, o el del meta-framework |
| Pruebas de componentes | Vue Test Utils + Vitest | Testing Library + Jest/Vitest |
| Renderizado en servidor | Nuxt | Next.js, Remix, otros |
| Herramientas de desarrollo | Vue DevTools | React DevTools |
| Lenguaje | JavaScript o TypeScript | JavaScript o TypeScript |
Esa columna de la izquierda explica por qué se dice que Vue tiene menos fragmentación, y merece un análisis honesto en las dos direcciones.
Ventajas reales: hay una respuesta canónica a cada pregunta; la documentación oficial cubre el conjunto y los ejemplos encajan entre sí; las actualizaciones están coordinadas; dos proyectos Vue de empresas distintas se parecen bastante, con lo que incorporarse a uno es más rápido; y no hay que dedicar una semana a evaluar cuatro librerías de enrutado.
Desventajas reales: menos innovación desde fuera, porque hay menos incentivo para competir con la solución oficial; menos opciones cuando tu caso es raro; y un ecosistema de terceros más pequeño en números absolutos —para una necesidad muy específica es más probable encontrar una librería React que una Vue—.
Es el eje librería-framework de 10-01 con Vue situado en el medio: más opiniones que React, menos que Angular. Y también aquí conviene desconfiar de las conclusiones automáticas: la fragmentación de React es un coste para un equipo pequeño y una ventaja para uno que necesita algo poco común.
- Nómada Tareas en Vue: la lista completa
La pantalla objetivo del módulo, tercera versión. El dominio es el mismo fichero JavaScript de 10-02 y 10-03, sin tocar:
// src/dominio/reglas.js — idéntico en las tres versiones
export const SIGUIENTE = Object.freeze({ pendiente: 'en-curso', 'en-curso': 'hecha', hecha: null });
export const ETIQUETA = Object.freeze({ pendiente: 'Empezar', 'en-curso': 'Marcar hecha', hecha: 'Hecha' });
export const PESOS = Object.freeze({ alta: 3, media: 2, baja: 1 });
export const HOY = '2026-09-20';
export const estaVencida = (t, hoy = HOY) => t.estado !== 'hecha' && t.fechaLimite < hoy;El filtro:
<!-- src/componentes/FiltroResponsable.vue -->
<script setup>
defineProps({
responsables: { type: Array, required: true },
modelValue: { type: String, default: null }
});
defineEmits(['update:modelValue']);
</script>
<template>
<label class="filtro" for="filtro-responsable">
Responsable:
<select
id="filtro-responsable"
:value="modelValue ?? ''"
@change="$emit('update:modelValue', $event.target.value || null)"
>
<option value="">Todos</option>
<option v-for="nombre in responsables" :key="nombre" :value="nombre">
{{ nombre }}
</option>
</select>
</label>
</template>La tarjeta es la del apartado 4. Y la lista:
<!-- src/componentes/ListaTareas.vue -->
<script setup>
import TarjetaTarea from './TarjetaTarea.vue';
defineProps({ tareas: { type: Array, required: true } });
defineEmits(['avanzar']);
</script>
<template>
<p v-if="tareas.length === 0" class="lista-vacia">
Ninguna tarea coincide con el filtro.
</p>
<ul v-else class="lista-tareas">
<TarjetaTarea
v-for="tarea in tareas"
:key="tarea.id"
:tarea="tarea"
@avanzar="$emit('avanzar', $event)"
/>
</ul>
</template>
<style scoped>
.lista-tareas { list-style: none; padding: 0; display: grid; gap: 0.5rem; }
.lista-vacia { color: var(--gris); font-style: italic; }
</style>Y la aplicación:
<!-- src/App.vue -->
<script setup>
import { usarTablero } from './composables/usarTablero.js';
import { usarFiltroResponsable } from './composables/usarFiltroResponsable.js';
import FiltroResponsable from './componentes/FiltroResponsable.vue';
import ListaTareas from './componentes/ListaTareas.vue';
import { BACKLOG } from './datos/backlog.js';
import { computed } from 'vue';
const { tareas, avanzar, resumen } = usarTablero(BACKLOG);
const { responsable, responsables, visibles } = usarFiltroResponsable(tareas);
const horasVisibles = computed(() =>
visibles.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);
</script>
<template>
<main class="tablero">
<header class="tablero__cabecera">
<h1>Nómada Tareas</h1>
<FiltroResponsable v-model="responsable" :responsables="responsables" />
<p class="tablero__resumen">
{{ visibles.length }} de {{ resumen.total }} tareas · {{ horasVisibles }} h abiertas
</p>
</header>
<ListaTareas :tareas="visibles" @avanzar="avanzar" />
</main>
</template>
<style scoped>
.tablero__cabecera { display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap; }
.tablero__resumen { color: var(--gris); margin-left: auto; }
</style>Los números canónicos, verificados: sin filtro, «6 de 6 tareas · 45 h abiertas». Con «Iván», «3 de 6 · 25 h». Con «Lucía», «1 de 6 · 14 h». Con «Marta», «2 de 6 · 6 h». Y al pulsar «Empezar» en la tarea 6, avanzar muta tarea.estado, el proxy avisa a los computed que leyeron esa propiedad, y se actualizan solo la tarjeta afectada y el resumen. Las otras cinco tarjetas no se vuelven a ejecutar siquiera.
- Comparación con React y con JavaScript puro
La tabla de las tres versiones de la misma pantalla:
| Concepto | JavaScript puro | React | Vue |
|---|---|---|---|
| Unidad | Módulo vista/*.js + <template> + CSS |
Componente de función .jsx |
Componente de un solo fichero .vue |
| Estilos aislados | No (clases globales) | No por defecto | Sí (scoped) |
| Estado | Objeto estado + render() manual |
useState |
ref |
| Derivados | Caché por versión escrita a mano | useMemo (opcional, para optimizar) |
computed (la forma natural) |
| Actualizar | render() a mano |
Reejecutar el componente | Reejecutar solo lo que lee el dato |
| Inmutabilidad | Recomendada | Obligatoria | No hace falta: se muta |
| Identidad en listas | data-id + reconciliar() |
key |
:key |
| Limpieza | destruir() invocado a mano |
return de useEffect |
onUnmounted |
| Dependencias de efectos | — | Array declarado a mano | Detectadas automáticamente |
| Eventos hacia arriba | CustomEvent |
Prop de función | emit declarado |
| Composición de contenido | <template> + <slot> de web components |
children |
slots con nombre y ámbito |
| Lógica reutilizable | Módulos y clases | Hooks (con reglas de orden) | Composables (sin reglas de orden) |
Y las métricas de la misma pantalla:
| Métrica | JavaScript puro | React | Vue |
|---|---|---|---|
| Líneas de la vista | ~210 | ~130 | ~120 |
| Líneas de infraestructura propia | ~60 | 0 | 0 |
| Ficheros de la vista | 5 (dom, tarjeta, tablero-vista, controlador, <template>) |
4 .jsx + 2 hooks |
4 .vue + 2 composables |
| Dependencias de producción | 0 | 2 | 1 |
| Peso del motor (comprimido, aprox.) | 0 | ~45 kB | ~35 kB |
| Ficheros a tocar para añadir un dato a la tarjeta | 2 | 1 | 1 (incluido su estilo) |
| Trabajo al cambiar el filtro | Reconciliar la lista | Ejecutar 6 componentes + comparar | Actualizar solo lo que cambió |
Y el balance cualitativo, sin declarar ganadores:
Vue gana en: concisión (menos ceremonia para lo mismo), estilos aislados sin convenciones de nombres, reactividad automática que elimina el array de dependencias, computed como forma natural en lugar de optimización, un ecosistema oficial coherente y una curva de aprendizaje más suave para quien viene de HTML y CSS.
React gana en: tamaño del mercado laboral y del ecosistema de terceros, un modelo mental más simple de explicar (es una función que se reejecuta), «todo es JavaScript» sin sintaxis de plantilla que aprender, mejor soporte de herramientas de terceros, y React Native para móvil nativo.
JavaScript puro gana en: cero dependencias, cero peso de motor, cero compilación obligatoria, control absoluto y un código que seguirá funcionando dentro de diez años sin actualizar nada.
Las contrapartidas de Vue, dichas con claridad: la reactividad automática falla en silencio cuando se pierde (apartado 15), y el fallo es «no se actualiza», que es más difícil de diagnosticar que un error; la sintaxis de plantilla es un lenguaje adicional que hay que aprender y que las herramientas genéricas no entienden sin complementos; y el mercado laboral es más pequeño que el de React en la mayoría de sitios, lo cual es un criterio legítimo aunque no sea técnico.
Errores Comunes y Consejos
Olvidar .value en el script. El error número uno. En la plantilla se desenvuelve solo, en el script no. if (responsable === 'Iván') compara un objeto ref con una cadena y siempre es falso, sin error. La regla de ESLint del complemento oficial de Vue lo detecta: actívala.
Desestructurar un objeto reactivo o un almacén de Pinia. const { responsable } = filtros copia el valor y rompe el vínculo. Usa toRefs para objetos reactivos y storeToRefs para almacenes.
Leer valores reactivos fuera del contexto de seguimiento. Dentro de un setTimeout, un .then() o un manejador registrado a mano, la lectura no se registra. Lee el valor antes, en el cuerpo del efecto.
Usar watch para derivar valores. Es el mismo antipatrón que useEffect para derivar estado en React: dos fuentes de verdad y un instante de desfase. Si es un valor, es computed.
Confundir v-if con v-show. v-show deja los nodos en el DOM: con 600 tareas, 600 nodos ocultos, encontrables con Ctrl+F, con el coste de memoria y de árbol de accesibilidad que mediste en 09-04. Para filtrar, v-if.
Usar el índice como :key. El mismo error que en React y que en tu reconciliar. Si la lista se filtra o se reordena, los nodos cambian de dato y se pierden el foco y el estado interno.
Olvidar .prevent en los formularios. @submit sin .prevent recarga la página. Es tan frecuente que Vue hizo el modificador precisamente por eso.
Olvidar .number en los campos numéricos. <input type="number" v-model="horas"> guarda una cadena. La validación de la R3 (horas > 0 && horas <= 40) compararía texto. Usa v-model.number.
Mutar props desde el hijo. Vue avisa en desarrollo. El hijo emite un evento; el padre decide qué hacer. Es el flujo unidireccional, que v-model no rompe sino que formaliza.
Creer que v-model es magia bidireccional. Es un v-bind más un v-on. Entenderlo así evita esperar comportamientos que no existen.
Consejo: usa ref por defecto y reactive solo cuando lo justifiques. ref funciona con cualquier tipo, se puede reemplazar entero, y el .value hace visible dónde está la reactividad.
Consejo: mantén el dominio fuera de los componentes. dominio/reglas.js es el mismo fichero en las tres versiones de esta pantalla, y esa es la mejor prueba de que la disciplina de 10-01 funciona: al cambiar de framework solo se reescribe la vista.
Consejo: instala Vue DevTools desde el primer día. Permite inspeccionar el árbol de componentes, ver el valor de cada ref y cada computed en tiempo real, y —muy útil— ver qué dependencias tiene registradas cada efecto, que es la forma directa de diagnosticar una reactividad perdida.
Ejercicios
Ejercicio 1 · Cazar las reactividades perdidas
Este componente tiene cuatro fallos de reactividad. Encuéntralos, explica por qué falla cada uno y escribe la versión corregida.
<script setup>
import { ref, reactive, computed, watch } from 'vue';
import { BACKLOG } from '../datos/backlog.js';
const filtros = reactive({ responsable: null, texto: '' });
const { responsable } = filtros;
const tareas = ref(BACKLOG);
const horasAbiertas = ref(0);
watch(tareas, (t) => {
horasAbiertas.value = t.filter((x) => x.estado !== 'hecha')
.reduce((s, x) => s + x.horasEstimadas, 0);
}, { immediate: true });
const visibles = computed(() => {
setTimeout(() => console.log(filtros.texto), 0);
return responsable === null
? tareas.value
: tareas.value.filter((t) => t.responsable === responsable);
});
function limpiar() {
filtros = { responsable: null, texto: '' };
}
</script>
<template>
<p>{{ visibles.length }} tareas · {{ horasAbiertas }} h</p>
<button @click="limpiar">Limpiar</button>
</template>Ejercicio 2 · Un composable con ciclo de vida
Escribe un composable usarTareasRemotas(responsable) que:
- Reciba un
refcon el responsable filtrado. - Cargue las tareas del servidor con tu
listarTareasde 07-02 cada vez que cambie el responsable. - Exponga
tareas,cargandoyerrorcomo refs. - Cancele la petición anterior al lanzar una nueva y al desmontar el componente, evitando la condición de carrera de 10-02.
- Exponga también un
computedcon las horas abiertas de las tareas cargadas.
Después, explica por qué esta versión no necesita ningún array de dependencias y qué error de React se evita con ello.
Ejercicio 3 · La misma pantalla, tres veces
Sin escribir código nuevo, rellena esta tabla comparando el mismo cambio funcional en las tres versiones de la pantalla que has visto en el módulo: añadir un badge que muestre el esfuerzo ponderado de cada tarea (horasEstimadas × PESOS[prioridad]), con fondo rojo si supera 30.
| Paso | JavaScript puro | React | Vue |
|---|---|---|---|
| Ficheros que hay que abrir | |||
| Dónde va el cálculo | |||
| Dónde va el marcado | |||
| Dónde va el estilo | |||
| Riesgo de olvidar algo | |||
| Qué se recalcula al cambiar el filtro |
Después responde: en la versión de JavaScript puro, ¿qué pasaría si añadieras el badge a pintarTarjeta pero olvidaras añadirlo también al <template> de index.html?
Soluciones
Solución 1
Fallo 1 · const { responsable } = filtros (línea 6). Desestructurar un objeto reactive copia el valor primitivo null y pierde el vínculo. responsable valdrá null para siempre, así que visibles nunca filtrará nada aunque el usuario cambie el <select>. Es el límite 1 del apartado 15.
Fallo 2 · watch para derivar horasAbiertas. Es el antipatrón del apartado 18: dos fuentes de verdad, un instante de desfase y más código. Además, como tareas es un ref a un array y el watch no es profundo, mutar el estado de una tarea no dispara el watch: horasAbiertas se quedaría obsoleta al marcar una tarea como hecha, que es justamente el caso de uso principal.
Fallo 3 · Lectura dentro de un setTimeout en el computed. La lectura de filtros.texto ocurre después de que el computed haya terminado, fuera del contexto de seguimiento: no se registra como dependencia. Además, un computed no debe tener efectos secundarios: debe ser una función pura de sus dependencias, igual que tus reductores de 10-03.
Fallo 4 · filtros = { … } en limpiar. Reasignar un objeto reactive rompe el vínculo con la plantilla, que sigue apuntando al proxy original. Además, filtros está declarado con const, así que esto lanzaría un TypeError en tiempo de ejecución (01-05).
Versión corregida:
<script setup>
import { ref, computed } from 'vue';
import { BACKLOG } from '../datos/backlog.js';
// ref por defecto: funciona con cualquier tipo y se puede reemplazar entero
const filtros = ref({ responsable: null, texto: '' });
const tareas = ref(BACKLOG);
// Derivado: computed, no watch
const horasAbiertas = computed(() =>
tareas.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);
// Puro y con todas las lecturas dentro del contexto de seguimiento
const visibles = computed(() => {
const { responsable, texto } = filtros.value; // lectura DENTRO: sí se registra
const t = texto.trim().toLowerCase();
return tareas.value
.filter((x) => responsable === null || x.responsable === responsable)
.filter((x) => t === '' || x.titulo.toLowerCase().includes(t));
});
function limpiar() {
filtros.value = { responsable: null, texto: '' }; // reemplazo del contenido del ref
}
</script>
<template>
<p>{{ visibles.length }} tareas · {{ horasAbiertas }} h</p>
<button @click="limpiar">Limpiar</button>
</template>Nota sobre la desestructuración de la línea const { responsable, texto } = filtros.value: aquí sí es correcta, porque ocurre dentro del computed. La lectura de filtros.value registra la dependencia en ese momento; lo que se desestructura después son valores ya leídos, usados inmediatamente. La regla no es «nunca desestructures», sino «no guardes valores desestructurados fuera de un contexto reactivo».
Solución 2
// src/composables/usarTareasRemotas.js
import { ref, computed, watchEffect } from 'vue';
import { listarTareas } from '../datos/api-tareas.js'; // tu módulo de 07-02
/**
* Carga las tareas del servidor filtradas por responsable.
* @param {import('vue').Ref<string|null>} responsable
*/
export function usarTareasRemotas(responsable) {
const tareas = ref([]);
const cargando = ref(false);
const error = ref(null);
watchEffect(async (alLimpiar) => {
const controlador = new AbortController();
alLimpiar(() => controlador.abort()); // ← antes de la siguiente ejecución y al desmontar
cargando.value = true;
error.value = null;
try {
// La lectura de responsable.value registra la dependencia AQUÍ, antes del await
tareas.value = await listarTareas({
responsable: responsable.value,
signal: controlador.signal
});
} catch (fallo) {
if (fallo.name === 'AbortError') return; // cancelación esperada: no es un error
error.value = fallo; // ErrorDeApi de 07-03
} finally {
cargando.value = false;
}
});
const horasAbiertas = computed(() =>
tareas.value.filter((t) => t.estado !== 'hecha')
.reduce((s, t) => s + t.horasEstimadas, 0)
);
return { tareas, cargando, error, horasAbiertas };
}Uso:
<script setup>
import { ref } from 'vue';
import { usarTareasRemotas } from './composables/usarTareasRemotas.js';
const responsable = ref(null);
const { tareas, cargando, error, horasAbiertas } = usarTareasRemotas(responsable);
</script>
<template>
<p v-if="cargando" role="status">Cargando tareas…</p>
<p v-else-if="error" role="alert">No se han podido cargar: {{ error.message }}</p>
<ul v-else :aria-busy="cargando">
<li v-for="t in tareas" :key="t.id">{{ t.titulo }}</li>
</ul>
<p>{{ horasAbiertas }} h abiertas</p>
</template>Por qué no hace falta array de dependencias. El watchEffect lee responsable.value durante su ejecución, y el proxy registra esa lectura automáticamente. Cuando el valor cambia, el efecto se vuelve a ejecutar — después de haber llamado a la limpieza, que aborta la petición en vuelo.
Qué error de React se evita. Dos, en realidad:
- La dependencia olvidada. En React, escribir
}, [])en lugar de}, [responsable])produce un componente que carga una vez y nunca se actualiza. Es tan frecuente que existe una regla de ESLint específica para ello. Aquí es imposible por construcción. - La dependencia que cambia siempre. Si en React pasaras un objeto
{ responsable }como dependencia, se recrearía en cada renderizado y la petición se lanzaría infinitamente. En Vue las dependencias son los valores reactivos leídos, no referencias comparadas.
Y un aviso, para no vender humo: hay un caso donde el seguimiento automático de Vue también falla. Las lecturas que ocurren después de un await no se registran, porque el contexto de seguimiento se cierra al suspenderse la función. Por eso el código lee responsable.value antes del await, dentro de la llamada. Si necesitaras leer otro ref después del await, tendrías que leerlo antes y guardarlo. Es el mismo límite del apartado 15, en su forma más sutil.
Solución 3
| Paso | JavaScript puro | React | Vue |
|---|---|---|---|
| Ficheros que hay que abrir | 3: index.html (<template>), js/vista/tarjeta.js, css/estilos.css |
2: TarjetaTarea.jsx, el CSS |
1: TarjetaTarea.vue |
| Dónde va el cálculo | En pintarTarjeta, como variable local |
En el cuerpo del componente, calculado en el render | En un computed del <script setup> |
| Dónde va el marcado | Añadir un <span> al <template> y rellenarlo en pintarTarjeta |
En el JSX, junto al resto | En el <template> del mismo fichero |
| Dónde va el estilo | Clase nueva en el CSS global, con prefijo para evitar colisiones | Clase en el CSS global, o módulo CSS | En el <style scoped>, sin riesgo de colisión |
| Riesgo de olvidar algo | Alto: tres ficheros y un acoplamiento por nombre de clase que nadie comprueba | Bajo | Muy bajo: todo está en la misma pantalla |
| Qué se recalcula al cambiar el filtro | Se reconcilian las tarjetas visibles y se recalcula el badge de cada una | Se ejecutan los 6 componentes; con memo, solo los que cambian |
Solo los computed cuyas dependencias cambiaron |
Qué pasa si olvidas el <span> en el <template> de index.html. Depende de cómo esté escrito pintarTarjeta. Si usa $('.tarea__esfuerzo', li).textContent = …, el selector devuelve null y la línea lanza un TypeError: Cannot set properties of null en tiempo de ejecución, al pintar la primera tarjeta: la aplicación se rompe entera. Si en cambio comprueba antes (const badge = $('.tarea__esfuerzo', li); if (badge) …), no pasa nada visible: el badge simplemente no aparece, sin ningún error, y el fallo solo se detecta mirando la pantalla o con una prueba de extremo a extremo (08-06).
Ese es, con nombres y apellidos, el acoplamiento sin comprobar del que hablaba el apartado 13 de 10-01. En la versión Vue el error es imposible: el marcado y el código que lo rellena son el mismo fichero, y el compilador de plantillas avisa si una variable no existe.
Conclusión
Has visto Vue en su versión moderna —Composition API y <script setup>— y, sobre todo, has visto el segundo modelo de reactividad de 10-01 funcionando de verdad.
Sabes qué significa framework progresivo y lo has comprobado: los mismos conceptos sirven para mejorar una página existente con una etiqueta <script> y sin compilar, o para montar una aplicación completa con Vite, Vue Router y Pinia. Conoces los Componentes de un Solo Fichero con sus tres bloques y por qué agrupar plantilla, lógica y estilos no viola la separación de intereses sino que la respeta: la estructura, el aspecto y el comportamiento de una tarjeta son un solo interés, y el <style scoped> convierte el aislamiento en una garantía del compilador en lugar de una convención de nombres.
Dominas la sintaxis de plantilla: la interpolación con su escapado automático, v-bind con las formas de objeto para clases y estilos —cuatro instrucciones imperativas de classList reducidas a una descripción—, v-on con sus modificadores que codifican el preventDefault y el stopPropagation de 06-03, la diferencia entre v-if y v-show —crear y destruir frente a display: none, con los 7.812 nodos frente a 1.194 que mediste en 09-04—, v-for con :key, que por tercera vez en el módulo es tu data-id de 06-06, y v-model desmitificado: un v-bind que baja el valor y un v-on que lo sube, el formulario controlado de 10-02 escrito en una línea, con .trim y .number que evitan errores reales en las reglas R2 y R3.
Entiendes la reactividad por dentro. Sabes por qué existe .value —JavaScript no permite interceptar la asignación a una variable, solo a una propiedad— y que un ref es un objeto con get/set interceptados, exactamente el mecanismo de 05-03. Sabes cómo los proxies registran las dependencias durante la ejecución de un efecto, con sus tres consecuencias: detección automática y precisa, detección dinámica que ignora las ramas no tomadas, y detección solo durante la ejecución — de donde salen los cuatro límites donde la reactividad se pierde: desestructurar un objeto reactivo, desestructurar props, pasar un primitivo a una función y reemplazar un reactive entero, con toRefs y storeToRefs como herramientas. Y tienes la comparación honesta: React te obliga a declarar dependencias y por eso puede comprobarlas con ESLint; Vue las descubre solo y por eso el fallo es silencioso.
Sabes que computed es tu caché por versión de 09-02 con la invalidación descubierta automáticamente —veinte líneas y un riesgo de olvido convertidos en una línea sin riesgo—, y que a diferencia de useMemo no es una optimización sino la forma natural de escribir derivados. Distingues watch y watchEffect con el criterio para elegir entre los tres, y reconoces el antipatrón de derivar con watch como el mismo error que derivar con useEffect. Conoces el ciclo de vida con onMounted y onUnmounted, que vuelve a ser tu destruir() con la llamada garantizada pero el contenido a tu cargo.
Sabes comunicar componentes con props y emits declarados —un contrato completo y validado que se lee en dos líneas—, componer contenido con slots con nombre y con ámbito, e inyectar valores con provide/inject, que a diferencia de Context no repinta a todos los consumidores porque la reactividad es de grano fino. Extraes lógica en composables sin reglas de orden de llamada, y sabes que la diferencia práctica más visible con React está en avanzar: donde React exige un map inmutable, Vue muta la propiedad y el proxy hace el resto. Conoces Pinia en su versión mínima —casi idéntica a un composable, con storeToRefs como requisito— y el ecosistema oficial que explica la menor fragmentación de Vue, con sus ventajas y sus costes.
Y has visto la tercera versión de la misma pantalla, con el mismo dominio/reglas.js sin tocar en las tres, ~120 líneas, un solo fichero por componente con sus estilos aislados, y actualizaciones que tocan solo lo que ha cambiado. Con las contrapartidas dichas: la reactividad perdida falla en silencio, la sintaxis de plantilla es un lenguaje más, y el mercado laboral es más pequeño.
Quedan dos modelos por ver. El siguiente es el más distinto de todos: un framework que no es una librería ni un framework progresivo sino una plataforma completa, con TypeScript como requisito de hecho, inyección de dependencias, una CLI que genera todo, observables para los flujos asíncronos, y —desde hace poco— señales muy parecidas a los ref que acabas de aprender. Es el extremo «con opiniones» del eje de 10-01: Conceptos Básicos de Angular.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
