La última lección del módulo anterior terminó con una lista incómoda: para que Nómada Tareas funcione como funciona has tenido que construir a mano un render declarativo, una reconciliación por clave estable, un estado centralizado con invalidación, una limpieza sistemática, una lista virtualizada y una división de código. Esa lista es, casi punto por punto, lo que un framework moderno te entrega resuelto el primer día. Pero «te lo dan resuelto» no es una explicación: es un eslogan. Esta lección es el diagnóstico antes del remedio. Vas a ver qué problemas concretos aparecen cuando se construye una interfaz a mano —los ocho que tú ya has sufrido y resuelto, con nombre y apellidos—, qué significa de verdad el salto de imperativo a declarativo y la ecuación UI = f(estado), cómo funcionan por dentro las tres familias de reactividad que se reparten el panorama, por qué el componente es una unidad de reutilización mejor que tus módulos de vista/, qué tipos de estado existen y por qué el estado del servidor es un problema de otra naturaleza, para qué sirve el ciclo de vida, qué separa una librería de un framework, y —lo que casi ningún tutorial cuenta— cuánto cuesta adoptar uno y cuándo no deberías. Al terminar tendrás criterio, que es exactamente lo que hace falta para leer las cuatro lecciones siguientes sin dejarte deslumbrar por ninguna.

Contenido

  1. El diagnóstico antes del remedio
  2. Los ocho problemas de construir una interfaz a mano
  3. La tabla del diagnóstico: problema → tu solución → cómo lo resuelve un framework
  4. Imperativo frente a declarativo
  5. UI = f(estado): la ecuación y sus consecuencias
  6. El mismo trozo de tablero, escrito de las dos formas
  7. Qué significa «reactividad» exactamente
  8. Familia 1: DOM virtual y reconciliación
  9. Familia 2: reactividad de grano fino con señales
  10. Familia 3: compilación en tiempo de construcción
  11. Las tres familias en una tabla
  12. El componente: plantilla, estado, comportamiento y estilos
  13. Por qué el componente es mejor unidad que tus módulos de vista/
  14. Estado: local, elevado, compartido y de servidor
  15. Por qué el estado del servidor es un problema distinto
  16. El ciclo de vida y por qué existe
  17. Librería frente a framework: qué significa «con opiniones»
  18. El coste real de adoptar un framework
  19. Cuándo NO usar un framework
  20. El panorama actual para situarse
  21. Qué hace este módulo y qué no hace
  22. Errores Comunes y Consejos
  23. Ejercicios
  24. Conclusión

  1. El diagnóstico antes del remedio

Hay dos formas de aprender un framework. La primera es la habitual: abrir la documentación, copiar el «hola mundo», aprender la sintaxis, y descubrir seis meses después —a veces nunca— qué problema resolvía cada pieza. La segunda es la que puedes permitirte tú, y solo tú, porque has hecho el camino largo: mirar primero el problema, comprobar que lo has sufrido, y solo entonces mirar la solución que propone cada herramienta.

La diferencia no es de estilo. Quien aprende por la primera vía acaba usando useMemo en todas partes «porque optimiza», metiendo todo el estado en Redux «porque es lo profesional» y eligiendo framework por lo que ha visto en una charla. Quien aprende por la segunda pregunta otra cosa: ¿qué problema concreto tengo yo, cuánto me cuesta ahora, y cuánto me costaría con esto?

Nómada Tareas es hoy una aplicación real: seis tareas en el tablero, 48 horas totales, 45 abiertas, 600 tareas en la prueba de carga, render() en 31 ms, 1.194 nodos, 58,3 kB en tres peticiones y un LCP de 1,9 s. Ninguno de esos números salió gratis. Cada uno costó una lección, y varios costaron dos. Ese coste es el dato que necesitas para juzgar cualquier framework: no se compara contra un ideal, se compara contra lo que ya tienes.

Una advertencia de método antes de empezar. Este módulo no va a hacerte experto en React, Vue o Angular. Cuatro lecciones no dan para eso, y quien te diga lo contrario te está vendiendo algo. Lo que sí va a darte es el modelo mental de cada uno: qué problema resuelve, con qué mecanismo, a cambio de qué. Con ese modelo, aprender cualquiera de ellos en serio pasa de ser un mes de desconcierto a una semana de leer documentación entendiendo lo que lees.

  1. Los ocho problemas de construir una interfaz a mano

Vamos a nombrarlos. No son «problemas de JavaScript»: son problemas de cualquier interfaz que muestre datos que cambian. Aparecen en Java Swing, en Android nativo, en iOS y en la web. Los frameworks web modernos no los inventaron; heredaron cuarenta años de intentos.

Problema 1 · La sincronización entre estado y pantalla. El dato vive en una variable; la pantalla lo muestra en un <span>. Cuando el dato cambia, alguien tiene que acordarse de actualizar el <span>. Si hay tres sitios que muestran las horas abiertas —el resumen, la cabecera y el título de la pestaña—, hay tres sitios que actualizar, y el fallo consiste en olvidarse de uno. Es el error más común de la programación de interfaces y el más difícil de detectar en pruebas, porque la pantalla no «falla»: miente.

Problema 2 · La identidad de los nodos al redibujar. La forma perezosa de resolver el problema 1 es redibujarlo todo. Funciona, pero destruye los nodos, y con ellos el foco, la selección de texto, la posición de desplazamiento, las transiciones CSS en curso y el estado interno del navegador (un <details> abierto, un vídeo reproduciéndose). Lo viste en 06-06 con replaceChildren.

Problema 3 · El rendimiento del redibujado completo. Con seis tareas nadie lo nota. Con 600 y un input que se dispara en cada tecla, redibujarlo todo son 310 ms de bloqueo por pulsación. Lo mediste en 09-01 y lo dejaste en 31 ms en 09-04.

Problema 4 · La limpieza. Cada addEventListener, cada setInterval, cada IntersectionObserver, cada suscripción a un WebSocket es una cosa que hay que apagar cuando su trozo de pantalla desaparece. Si no se apaga, tienes una fuga de memoria y, peor, un manejador que sigue reaccionando a eventos de algo que ya no existe. Fue todo el 09-03, con destruir() y AbortController.

Problema 5 · La composición. Una tarjeta de tarea aparece en el tablero, en el panel de detalles y en el informe. Si es una función suelta que recibe un <li> y lo modifica, reutilizarla en otro contexto exige que ese contexto sepa demasiado sobre ella. La reutilización de trozos de interfaz con su estado y su comportamiento dentro no es un problema resuelto por funciones sueltas.

Problema 6 · La comunicación entre partes lejanas. El filtro por responsable lo cambia un <select> de la cabecera, y afecta a la lista, al resumen, al contador de la pestaña y a la URL. Tú lo resolviste con CustomEvent en 06-04, que funciona pero convierte el flujo de datos en algo que solo se puede seguir buscando cadenas de texto por todo el proyecto.

Problema 7 · El acoplamiento entre estructura, estilo y comportamiento. El HTML de la tarjeta está en un <template> de index.html, sus estilos en css/estilos.css y su comportamiento repartido entre tarjeta.js y controlador.js. Son cuatro ficheros para una sola cosa. Cambiar el nombre de una clase CSS exige tocar dos de ellos y confiar en la memoria.

Problema 8 · Las convenciones de equipo. Tú decidiste que las vistas exponen render, actualizar y destruir; que los eventos personalizados viven en EVENTOS; que las acciones se declaran con data-accion. Son buenas decisiones, pero son tuyas. Otra persona que entre al proyecto tiene que aprenderlas leyendo el código, porque no están escritas en ningún sitio salvo en la costumbre.

  1. La tabla del diagnóstico: problema → tu solución → cómo lo resuelve un framework

Esta es la tabla central de la lección. Léela despacio: la columna del medio es tuya, y por eso la de la derecha se entiende.

# Problema Tu solución en Nómada Tareas Cómo lo resuelve un framework
1 Sincronizar estado y pantalla El ciclo estado → render() → evento → nuevo estado → render() de 06-06, con una única función que describe la pantalla entera De fábrica: describes la pantalla en función del estado y el framework se encarga de volver a ejecutarla cuando el estado cambia
2 Identidad de los nodos reconciliar(contenedor, datos, clave, pintar) con data-id (06-06) La key de React, el :key de Vue y el track de Angular. Es literalmente tu data-id, elevado a requisito del API
3 Rendimiento del redibujado Índice Map, caché por versión (09-02), lotes de escritura y requestAnimationFrame (09-04), virtualización (09-04) Reconciliación incremental o actualización de grano fino; la virtualización sigue siendo tuya (con librerías)
4 Limpieza destruir() en cada vista + AbortController compartido (09-03) El ciclo de vida: la función de limpieza de useEffect, onUnmounted, ngOnDestroy. Se llama sola
5 Composición Módulos vista/*.js que exportan funciones y clases Componentes: plantilla, estado, comportamiento y estilos en una sola unidad instanciable
6 Comunicación entre partes lejanas CustomEvent + EVENTOS (06-04) props hacia abajo, eventos hacia arriba, y un almacén compartido cuando eso no basta (lo verás en 10-03)
7 Acoplamiento estructura/estilo/comportamiento Cuatro ficheros por tarjeta: <template>, CSS, tarjeta.js, controlador.js Un fichero por componente, con estilos con ámbito propio
8 Convenciones de equipo Las tuyas, sin escribir Las del framework, documentadas, con herramientas y con gente que ya las conoce

Dos lecturas de esta tabla, ambas importantes.

La primera: ninguna fila dice «imposible». Todo lo que hace un framework se puede hacer a mano, y tú lo has hecho. La diferencia no es de capacidad, es de coste marginal: la fila 2 te costó veinte líneas y media lección; en React es una propiedad key que escribes sin pensar.

La segunda, menos evidente: las filas 7 y 8 no son técnicas. Son de organización. Y en un equipo de cinco personas suelen pesar más que las seis primeras juntas, porque el coste real de un proyecto no está en escribir el código sino en que cinco personas lo entiendan a la vez durante tres años.

  1. Imperativo frente a declarativo

Aquí está el salto conceptual del módulo, y conviene definirlo bien porque las dos palabras se usan mal continuamente.

Programación imperativa: describes los pasos para llegar al resultado. «Coge el nodo con id 3, quítale la clase tarea--pendiente, ponle tarea--hecha, cambia el texto del botón a "Reabrir", deshabilita el otro botón y actualiza el contador de la columna restándole una unidad.»

Programación declarativa: describes el resultado y dejas que otro calcule los pasos. «Una tarea en estado hecha se ve así.» Si el estado es hecha, la pantalla muestra eso; cómo se llega desde lo que había antes no es asunto tuyo.

Ya conoces la distinción sin haberla nombrado. En 04-04 comparaste esto:

// Imperativo: los pasos
const abiertas = [];
for (let i = 0; i < backlog.length; i++) {
  if (backlog[i].estado !== 'hecha') {
    abiertas.push(backlog[i]);
  }
}

con esto:

// Declarativo: el resultado
const abiertas = backlog.filter((t) => t.estado !== 'hecha');

El segundo no es «más corto»: es de otra naturaleza. No menciona el índice i, ni el array de destino, ni el orden del recorrido. Describe qué es abiertas (las tareas cuyo estado no es 'hecha') y deja los pasos al motor. Si mañana JavaScript decidiera paralelizar filter, tu código seguiría siendo correcto; el bucle con índices, no necesariamente.

Aplicar esa misma distinción a la pantalla es lo que hacen los frameworks. Y hay una asimetría brutal en el número de casos que hay que considerar.

  • En modo imperativo, para pasar de un estado a otro tienes que escribir la transición. Con n estados posibles, hay n×(n−1) transiciones. Con cinco estados de una tarjeta (pendiente, en curso, hecha, vencida, sin asignar) son 20 transiciones. Nadie las escribe todas; se escriben las que se recuerdan, y los fallos viven en las que no.
  • En modo declarativo, escribes n descripciones: cómo se ve cada estado. Las transiciones las calcula el framework. Cinco descripciones en lugar de veinte transiciones.

Ese es el argumento entero. No es elegancia: es que el número de cosas que tienes que escribir a mano pasa de crecer al cuadrado a crecer de forma lineal.

graph LR
  subgraph Imperativo
    A1[Estado A] -->|transición 1| B1[Estado B]
    B1 -->|transición 2| C1[Estado C]
    A1 -->|transición 3| C1
    C1 -->|transición 4| A1
    B1 -->|transición 5| A1
    C1 -->|transición 6| B1
  end
  subgraph Declarativo
    A2[Estado A] --> V[vista describe el estado]
    B2[Estado B] --> V
    C2[Estado C] --> V
  end

  1. UI = f(estado): la ecuación y sus consecuencias

La forma condensada de todo lo anterior es una ecuación que verás en la documentación de casi todos los frameworks modernos:

UI = f(estado)

La interfaz es el resultado de aplicar una función pura al estado. Con el mismo estado, la misma pantalla, siempre. Sin estado oculto en el DOM, sin «depende de lo que hubiera antes».

Las consecuencias son más profundas de lo que parece:

  1. La pantalla se vuelve razonable. Para saber por qué se ve algo, miras el estado. No tienes que reconstruir mentalmente la secuencia de clics que llevó hasta ahí. Esto es exactamente lo que hacía tan difícil depurar interfaces imperativas: el bug no estaba en el estado actual sino en una transición que ocurrió hace treinta segundos.
  2. La pantalla se vuelve comprobable. Si f es una función, se prueba como una función: le das un estado, compruebas la salida. Es lo que ya hiciste en 08-05 con Testing Library, renderizando la vista con un tablero conocido y consultando por rol.
  3. El DOM deja de ser la fuente de la verdad. Este es el cambio mental más grande. En una interfaz imperativa, «¿cuántas tareas hay?» a veces se responde contando <li>. En una declarativa, eso es un error conceptual: el <li> es una consecuencia, no un dato. La fuente es el estado.
  4. Vuelve el problema del rendimiento. Si f produce la pantalla entera, ejecutarla en cada cambio significa recrearlo todo. Aquí es donde entra la reactividad, que es el mecanismo con el que cada framework evita pagar ese precio. De eso van los apartados 7 a 11.

Conviene ser honesto con una cosa: UI = f(estado) es un modelo, no una descripción literal. Hay estado que el DOM guarda de verdad y que la función no controla: la posición del cursor en un <input>, el desplazamiento de un contenedor, qué elemento tiene el foco. Los frameworks se esfuerzan mucho en preservar ese estado mientras hacen ver que redibujan todo, y ese esfuerzo es precisamente la reconciliación. Cuando falla —y falla— aparecen los fallos raros: un <input> que pierde el foco al escribir, una animación que se reinicia. Reconocerás la causa inmediatamente, porque es tu problema 2.

  1. El mismo trozo de tablero, escrito de las dos formas

Nada de esto se entiende sin código. Tomemos la operación más simple de Nómada Tareas: marcar la tarea 6 como hecha, con sus consecuencias visibles (la tarjeta cambia de aspecto, el botón cambia de texto, el contador de pendientes baja, el de hechas sube, y las horas abiertas pasan de 45 a 40).

La versión imperativa, escrita a mano

// Imperativo: modificamos la pantalla paso a paso
function marcarHechaImperativo(id) {
  const tarea = tablero.buscarPorId(id);
  tarea.cambiarEstado('hecha');                       // 1 · el modelo

  const li = document.querySelector(`[data-id="${id}"]`);
  li.dataset.estado = 'hecha';                        // 2 · el atributo
  li.classList.add('tarea--hecha');                   // 3 · la clase
  li.classList.remove('tarea--vencida');              // 4 · ya no puede estar vencida (R10)

  const avanzar = li.querySelector('[data-accion="avanzar"]');
  avanzar.disabled = true;                            // 5 · no hay estado siguiente
  avanzar.textContent = 'Hecha';                      // 6 · el texto del botón

  const reabrir = li.querySelector('[data-accion="reabrir"]');
  reabrir.disabled = true;                            // 7 · desde 'hecha' no se reabre (R6)

  const columnaHechas = document.querySelector('[data-columna="hecha"] .lista-tareas');
  columnaHechas.append(li);                           // 8 · mover de columna

  actualizarContador('pendiente');                    // 9 · contadores
  actualizarContador('hecha');                        // 10
  document.querySelector('#horas-abiertas').textContent =
    `${tablero.horasAbiertas()} h`;                   // 11 · el resumen
  document.title = `Nómada Tareas (${tablero.resumen().pendientes})`;  // 12 · la pestaña
}

Doce pasos. Todos correctos. Y ahora la pregunta que importa: ¿qué pasa si mañana añadimos un badge de prioridad a la tarjeta? Respuesta: hay que acordarse de actualizarlo aquí, y también en marcarEnCursoImperativo, y en reabrirImperativo, y en asignarResponsableImperativo. Cuatro sitios. El fallo consiste en tocar tres.

La versión declarativa, la que ya escribiste

// Declarativo: describimos la pantalla y volvemos a describirla
function marcarHechaDeclarativo(id) {
  tablero.cambiarEstado(id, 'hecha');   // 1 · el modelo, y solo el modelo
  render();                              // 2 · vuelve a describir la pantalla entera
}

function render() {
  const visibles = tareasVisibles(estado);

  reconciliar(lista, visibles, (t) => t.id, (t, nodo) => pintarTarjeta(t, nodo, HOY));

  $('#horas-abiertas').textContent = `${estado.tablero.horasAbiertas()} h`;
  document.title = `Nómada Tareas (${estado.tablero.resumen().pendientes})`;
}

Dos pasos. Y el badge de prioridad del ejemplo anterior se añade en un solo sitio: dentro de pintarTarjeta, que es la descripción de cómo se ve una tarea. Todas las acciones que provoquen un render() lo verán actualizado, sin que nadie tenga que acordarse.

Compara honestamente las dos:

Aspecto Imperativo Declarativo
Líneas por acción ~12, y crecen con la interfaz 2, constantes
Sitios que tocar al añadir un dato visible Uno por cada acción existente Uno, la descripción
Riesgo de pantalla desincronizada Alto: depende de la memoria Nulo por construcción
Trabajo del navegador Mínimo: solo lo que cambia Potencialmente mucho: hay que compararlo todo
Facilidad de depuración Baja: hay que reconstruir la secuencia Alta: mira el estado
Facilidad de prueba Baja: hay que simular la secuencia Alta: estado → salida

Fíjate en la única fila donde gana el imperativo: el trabajo del navegador. Esa fila es la factura del modelo declarativo, y es exactamente la que pagaste con reconciliar, con la caché por versión y con la virtualización. Los frameworks pagan esa misma factura, con mecanismos distintos. Vamos a ellos.

graph TD
  E["Estado<br/>tablero + filtros"] --> F["f(estado)<br/>descripción de la pantalla"]
  F --> R["Motor de reconciliación<br/>calcula el mínimo cambio"]
  R --> D["DOM real"]
  D -->|evento del usuario| A["Acción"]
  A -->|nuevo estado| E

  1. Qué significa «reactividad» exactamente

«Reactivo» es la palabra más gastada del vocabulario del frontend. Su significado técnico, sin embargo, es preciso:

Un sistema es reactivo cuando una dependencia declarada entre dos valores se mantiene automáticamente: si cambia el origen, el destino se actualiza sin que nadie lo pida.

El ejemplo canónico no es de programación, es de una hoja de cálculo. Escribes en C1 la fórmula =A1+B1. Cambias A1 y C1 se actualiza. No has «llamado» a nada: declaraste una relación y el sistema la mantiene. Esa es toda la idea.

En una interfaz hay dos relaciones que mantener:

  1. Estado → estado derivado: si cambian las tareas, cambian las horas abiertas, el número de pendientes y la lista filtrada.
  2. Estado → pantalla: si cambian las horas abiertas, cambia el <span> que las muestra.

Tú mantienes la primera con la caché por versión de 09-02 (recalcular cuando el contador de versión no coincide) y la segunda llamando a render() a mano. Funciona, pero tiene dos agujeros que conoces bien: si olvidas #invalidar() en una mutación, la caché miente; y si olvidas llamar a render(), la pantalla miente. Un sistema reactivo elimina los dos olvidos posibles. Ese es su valor entero.

La pregunta interesante es cómo se entera el sistema de que algo ha cambiado. Y ahí es donde el mundo se divide en tres familias.

  1. Familia 1: DOM virtual y reconciliación

Quién la usa: React (y, con matices, Preact e Inferno).

El mecanismo. Tu función f(estado) no toca el DOM. Devuelve una descripción de la pantalla: un árbol de objetos JavaScript ligeros, con el tipo de cada elemento, sus atributos y sus hijos. Ese árbol es el DOM virtual. Es la misma idea que tu pintarTarjeta, pero en lugar de crear un <li> real crearía algo así:

// Lo que devolvería una descripción virtual de una tarjeta
{
  tipo: 'li',
  props: { className: 'tarea tarea--alta', 'data-id': 6 },
  hijos: [
    { tipo: 'h3', props: { className: 'tarea__titulo' }, hijos: ['Presupuesto de la carpintería'] },
    { tipo: 'button', props: { 'data-accion': 'avanzar' }, hijos: ['Empezar'] }
  ]
}

Cuando el estado cambia, el framework vuelve a ejecutar f, obtiene un árbol nuevo, y compara el nuevo con el anterior (el diffing). De esa comparación sale una lista de operaciones mínimas sobre el DOM real: «cambia este texto», «quita esta clase», «elimina este nodo». Solo esas operaciones se aplican.

Por qué necesita claves. Comparar dos listas de hijos es un problema difícil en el caso general. El algoritmo óptimo de diferencia entre árboles es de coste cúbico, inviable. Los frameworks usan heurísticas de coste lineal, y la más importante es: si dos elementos ocupan la misma posición y tienen el mismo tipo, son el mismo elemento. Esa heurística falla exactamente cuando la lista se reordena o se elimina un elemento del medio — el problema 2, el que resolviste con data-id. La solución es idéntica a la tuya: pedir al programador una clave estable que identifique cada elemento independientemente de su posición. En React se llama key.

Las contrapartidas, con honestidad:

  • Se paga memoria: hay dos árboles virtuales vivos en cada actualización.
  • Se paga CPU: hay que construir el árbol nuevo entero y recorrerlo comparando, aunque solo haya cambiado una palabra. Con 600 tareas, eso son 600 descripciones creadas para descubrir que una cambió.
  • El coste no depende de cuánto ha cambiado, sino de cuánto hay. Ese es su talón de Aquiles, y la razón por la que aparecen useMemo, React.memo y compañía: son formas de decirle al framework «no bajes por esta rama, no ha cambiado».
  • A cambio, el modelo mental es muy simple: es una función que se vuelve a ejecutar. No hay suscripciones invisibles ni objetos envueltos en proxies. Cuando algo va mal, lo que ha pasado es que la función se ha ejecutado, o no se ha ejecutado.

  1. Familia 2: reactividad de grano fino con señales

Quién la usa: Vue (con ref/reactive/computed), Angular moderno (signal), Solid, Svelte en su versión con runas, Preact Signals y prácticamente todo lo nuevo.

El mecanismo. En lugar de comparar árboles, el sistema registra qué depende de qué mientras se ejecuta. Un valor reactivo (una señal) no es un valor: es un valor con una lista de suscriptores. Cuando se lee dentro de un contexto de seguimiento, la señal apunta ese contexto como dependiente suyo. Cuando se escribe, notifica a todos sus dependientes.

En seudocódigo, y simplificando mucho:

// Una implementación mínima de señal, para entender el mecanismo
let observadorActual = null;

function señal(valorInicial) {
  let valor = valorInicial;
  const suscriptores = new Set();

  return {
    get() {
      if (observadorActual) suscriptores.add(observadorActual);  // ← registro automático
      return valor;
    },
    set(nuevo) {
      if (Object.is(valor, nuevo)) return;      // sin cambio, sin trabajo
      valor = nuevo;
      for (const s of [...suscriptores]) s();   // ← notificación
    }
  };
}

function efecto(fn) {
  const ejecutar = () => {
    observadorActual = ejecutar;   // mientras corre fn, las lecturas se apuntan
    try { fn(); } finally { observadorActual = null; }
  };
  ejecutar();
}

Con eso ya funciona lo esencial:

const horasAbiertas = señal(45);

efecto(() => {
  document.querySelector('#horas-abiertas').textContent = `${horasAbiertas.get()} h`;
});

horasAbiertas.set(40);   // el <span> se actualiza solo: nadie ha llamado a render()

Lee otra vez ese último bloque, porque contiene la idea entera de la familia. Nadie ha llamado a render(). El efecto se apuntó como dependiente al leer la señal, y la señal lo avisó al cambiar. No hay comparación de árboles, no hay recorrido: hay una lista de suscriptores y una llamada.

La consecuencia decisiva: el coste de una actualización es proporcional a cuánto ha cambiado, no a cuánto hay en pantalla. Cambiar las horas abiertas actualiza un nodo de texto, tanto si el tablero tiene 6 tareas como si tiene 600. Es, conceptualmente, la misma diferencia que hubo en 09-02 entre recorrer un array y consultar un Map.

Las contrapartidas, con la misma honestidad:

  • El seguimiento tiene coste de memoria y de establecimiento: cada valor reactivo arrastra su lista de suscriptores.
  • El modelo mental es más sutil. Como el registro ocurre al leer, hay formas de «perder» la reactividad sin darte cuenta: desestructurar un objeto reactivo copia el valor y rompe el vínculo; leer dentro de un setTimeout no se registra porque el contexto ya se cerró. Son los errores clásicos de Vue, y los verás con nombre y ejemplo en 10-04.
  • Cuando algo no se actualiza, la causa es invisible: no hay una llamada que falte, hay una dependencia que no se registró. Depurar eso requiere herramientas específicas.
  • A cambio, el rendimiento por defecto es mejor y hacen falta muchas menos optimizaciones manuales. En la práctica, useMemo tiene menos equivalentes necesarios en el mundo de las señales.

  1. Familia 3: compilación en tiempo de construcción

Quién la usa: Svelte fue el primero en hacerlo su bandera; Solid compila las plantillas JSX a operaciones directas del DOM; Angular compila sus plantillas desde siempre; Vue compila las suyas y usa lo aprendido para saltarse trozos del árbol que no pueden cambiar.

El mecanismo. Las dos familias anteriores resuelven el problema en el navegador, en tiempo de ejecución. Esta lo resuelve antes: un compilador lee tu componente durante la construcción del proyecto y genera código JavaScript que actualiza el DOM directamente, sin motor genérico que lo interprete.

Si escribes una plantilla que dice «aquí va el número de horas abiertas», el compilador puede ver que ese es el único punto que depende de esa variable y generar, literalmente, nodo7.textContent = horasAbiertas. No hay comparación ni suscripción genérica: hay una asignación, escrita por una máquina que ya sabía dónde estaba el hueco.

Las contrapartidas:

  • Ventaja grande: el motor casi desaparece del paquete. Lo que se descarga es tu código compilado más un puñado de utilidades, no un motor de reconciliación completo. Es la razón por la que los frameworks compilados tienen paquetes base tan pequeños.
  • Ventaja segunda: el trabajo de análisis se hace una vez en tu máquina, no un millón de veces en los móviles de los usuarios.
  • Desventaja: el código que escribes no es JavaScript. Es un lenguaje de plantillas que se parece al HTML y que solo entiende ese compilador. Las herramientas (editor, ESLint, comprobador de tipos, depurador) necesitan complementos específicos, y el código que ves en el depurador no es el que escribiste.
  • Desventaja segunda: la magia se paga en sorpresas. Cuando el compilador no puede saber estáticamente de qué depende algo, tiene que recurrir a mecanismos de tiempo de ejecución, y ahí surgen reglas raras que hay que memorizar.
  • En la práctica las tres familias se están mezclando: React tiene un compilador que inserta memoización automáticamente; Vue compila y usa señales a la vez; Angular compila y ha adoptado señales. La frontera es cada vez menos nítida.

  1. Las tres familias en una tabla

Criterio DOM virtual Señales (grano fino) Compilación
Cuándo se decide qué actualizar En ejecución, comparando árboles En ejecución, siguiendo dependencias En construcción, analizando la plantilla
Unidad que se vuelve a ejecutar El componente entero La expresión concreta que depende del dato La instrucción generada
Coste de una actualización Proporcional al tamaño del árbol Proporcional a lo que ha cambiado Mínimo: asignación directa
Necesita claves en listas Sí (key) Sí (:key, track)
Tamaño del motor descargado Mayor Medio Muy pequeño
Modelo mental Simple: es una función que se reejecuta Sutil: hay dependencias implícitas Opaco: el código real lo escribe el compilador
Optimización manual habitual Frecuente (memoizar, evitar renders) Poco frecuente Casi nunca
Fallos típicos Renders de más, key incorrecta Reactividad perdida al desestructurar Reglas del compilador poco intuitivas
Herramientas de terceros Máximas Buenas Requieren soporte específico
Ejemplos React Vue, Angular, Solid Svelte, Solid, Angular

Una advertencia que ahorra discusiones estériles: ninguna de las tres es «la correcta». Cada una optimiza algo distinto. El DOM virtual optimiza la simplicidad del modelo mental y la libertad (todo es JavaScript normal). Las señales optimizan el rendimiento por defecto. La compilación optimiza el tamaño y el arranque. En la mayoría de las aplicaciones reales, con listas de decenas de elementos, las tres son suficientemente rápidas y la decisión se toma por otros motivos: equipo, ecosistema, contratación.

  1. El componente: plantilla, estado, comportamiento y estilos

El segundo concepto grande del módulo, y el que más se parece a algo que ya sabes de otras partes de la ingeniería: la unidad de reutilización.

Un componente es una unidad que agrupa cuatro cosas que hasta ahora tenías separadas:

Parte Qué es En Nómada Tareas hoy
Plantilla La estructura HTML que produce <template id="plantilla-tarea"> en index.html
Estado Los datos que solo le importan a él Repartido: unos en estado, otros en atributos del DOM
Comportamiento Qué hace al recibir eventos controlador.js, por delegación
Estilos Su aspecto Un trozo de css/estilos.css, con clases .tarea__*

Un componente los pone juntos y con un contrato explícito: recibe datos por sus props, emite eventos hacia arriba, y ocupa un trozo de pantalla del que es enteramente responsable.

Tres propiedades hacen que esta unidad funcione tan bien:

  1. Se instancia. No es «la tarjeta»: es una plantilla de la que se crean seis tarjetas, cada una con su propio estado interno (por ejemplo, si tiene el menú de acciones desplegado). Con tu pintarTarjeta, ese estado por tarjeta no tiene sitio natural donde vivir y acaba en un dataset o en un Map externo.
  2. Se compone. Un componente contiene otros componentes, y el resultado sigue siendo un componente. <Tablero> contiene tres <Columna>, cada una contiene n <TarjetaTarea>. Es exactamente lo mismo que hace el HTML, pero con tus propias etiquetas.
  3. Aísla. Los estilos con ámbito propio impiden que la clase .titulo de la tarjeta afecte a la .titulo de la cabecera. Es el equivalente visual de lo que los módulos ES de 05-04 hicieron con los nombres de variables: acabar con el espacio de nombres global.

  1. Por qué el componente es mejor unidad que tus módulos de vista/

Comparemos con lo que tienes. js/vista/tarjeta.js exporta pintarTarjeta(tarea, existente, hoy). Es una buena función: pura en su intención, sirve para crear y actualizar, y no depende del resto. Pero mira sus costuras:

// El contrato real de tu tarjeta, repartido por cuatro sitios
pintarTarjeta(tarea, existente, hoy)          // js/vista/tarjeta.js
<template id="plantilla-tarea">…</template>   // index.html  ← acoplamiento por id global
.tarea, .tarea--alta, .tarea__titulo …        // css/estilos.css ← acoplamiento por nombre de clase
[data-accion="avanzar"]                       // js/vista/controlador.js ← acoplamiento por atributo

Cuatro acoplamientos, y ninguno lo comprueba nadie. Si renombras plantilla-tarea, nada falla hasta que se ejecuta. Si cambias .tarea__titulo en el CSS, la tarjeta se ve mal pero no hay error. Si escribes data-accion="avansar", el botón simplemente no hace nada. Son fallos que solo detecta una prueba de extremo a extremo, y por eso te costaron tres recorridos de Cypress en 08-06.

Ahora los tres problemas que un componente resuelve y tu función no:

Estado por instancia. Si cada tarjeta necesita saber si su menú está desplegado, ¿dónde vive ese dato? En tu diseño, o lo metes en el objeto Tarea (contaminando el modelo con asuntos de la vista), o lo guardas en un Map de la vista indexado por id (y te acuerdas de limpiarlo cuando la tarea desaparece, o tienes la fuga de 09-03). En un componente, es una variable local del componente y desaparece con él.

Ciclo de vida por instancia. Si la tarjeta de una tarea vencida necesita un temporizador que actualice «vencida hace 15 d» a medianoche, hoy tienes que crear el temporizador en algún sitio y acordarte de cancelarlo cuando la tarjeta se elimina en reconciliar. Tu reconciliar no avisa a nadie de que ha eliminado un nodo. En un componente, hay un punto de desmontaje que se llama solo.

Contrato explícito. pintarTarjeta(tarea, existente, hoy) no dice qué campos de tarea usa. Un componente declara sus props, y con TypeScript (que verás asomar en 10-05) esa declaración se comprueba en la compilación.

Dicho lo cual, seamos justos: tu módulo vista/ es lo correcto para el tamaño de Nómada Tareas. Seis tareas, una pantalla, un desarrollador. El componente empieza a compensar cuando hay veinte tipos de elemento reutilizables, cada uno con estado propio, y varias personas tocándolos a la vez.

  1. Estado: local, elevado, compartido y de servidor

El tercer concepto grande. Casi todo el sufrimiento de las aplicaciones grandes viene de no distinguir cuatro cosas que se llaman igual.

Tipo Qué es Ejemplo en Nómada Tareas Dónde debe vivir
Local Solo le importa a un trozo de interfaz Si el menú de acciones de una tarjeta está desplegado Dentro del componente
Elevado Lo comparten dos hermanos, así que sube al padre común El filtro de responsable, que afectan el <select> y la lista En el ancestro común más cercano
Compartido (global) Lo necesitan partes muy alejadas del árbol El usuario identificado, el tema claro/oscuro, el idioma En un almacén compartido
De servidor Es una copia local de un dato que vive en otro sitio Las tareas que devuelve listarTareas() de 07-02 En una caché con reglas propias

La regla práctica, en un orden que ahorra mucho trabajo: empieza siempre por local, y sube solo cuando te obliguen. El error más caro y más frecuente del frontend moderno es el contrario: meterlo todo en un almacén global desde el primer día «por si acaso». El resultado es un objeto gigante donde todo depende de todo, donde nada se puede borrar por miedo, y donde una prueba unitaria necesita montar la aplicación entera.

Los dos primeros tipos ya los conoces sin nombrarlos: tu objeto estado de app.js, con { tablero, filtros, orden }, es exactamente estado elevado, subido hasta arriba del todo porque lo comparten la lista, el resumen y el enrutador. Y funciona. La pregunta de 10-03 será: ¿a partir de qué tamaño deja de funcionar?

  1. Por qué el estado del servidor es un problema distinto

Esta distinción es de las más útiles de toda la lección y la entendió tarde toda la industria.

El estado local es tuyo: tú lo creas, tú lo cambias, tú eres la única fuente de verdad. El estado del servidor no es tuyo. Tú tienes una copia, obtenida en un momento concreto, de algo que vive en otra máquina y que puede cambiar sin avisarte. Eso cambia por completo las preguntas que hay que responder:

Pregunta Estado local Estado del servidor
¿Quién es la fuente de la verdad? Tu aplicación El servidor
¿Puede quedar obsoleto? No Sí, en cualquier momento
¿Hay que volver a pedirlo? Nunca Sí: al enfocar la ventana, al reconectar, cada n segundos
¿Puede fallar al leerlo? No Sí: red, 500, timeout
¿Hay estados intermedios? No Sí: cargando, error, reintentando, obsoleto-pero-mostrable
¿Hay concurrencia? Poca Sí: dos peticiones que vuelven desordenadas
¿Se comparte entre pantallas? A veces Casi siempre, y conviene una sola caché

Tú ya has vivido todas esas filas. En 07-03 escribiste pedirJson con ErrorDeApi, AbortController, timeouts y reintentos. En 07-04 lidiaste con la reconexión del CanalTablero y con el lote de actualizaciones que llega después. En 07-03 hiciste optimistic UI: pintar el cambio antes de que el servidor confirme, y revertirlo si falla.

La conclusión, que retomarás en 10-03, es que guardar el estado del servidor en el mismo sitio que el estado de la interfaz es un error de categoría. Son problemas con reglas distintas y merecen herramientas distintas. Por eso existen TanStack Query, RTK Query, SWR o los resources de Angular: no son «Redux pero moderno», son cachés de estado remoto con invalidación, reintento y deduplicación. Verás el concepto en 10-02 y 10-03.

  1. El ciclo de vida y por qué existe

Un trozo de interfaz pasa por tres momentos, y hay trabajo que solo puede hacerse en cada uno:

stateDiagram-v2
  [*] --> Montado: aparece en pantalla
  Montado --> Actualizado: cambia el estado o las props
  Actualizado --> Actualizado: vuelve a cambiar
  Montado --> Desmontado: desaparece de la pantalla
  Actualizado --> Desmontado: desaparece de la pantalla
  Desmontado --> [*]
Momento Qué se hace ahí Tu equivalente hoy
Montar Pedir datos, suscribirse al WebSocket, arrancar un IntersectionObserver, medir el DOM real El constructor de TableroVista y su primer render()
Actualizar Volver a pintar; reaccionar a un cambio de props (por ejemplo, cambia el id de la tarea mostrada) actualizar() + reconciliar()
Desmontar Apagarlo todo: quitar escuchas, cancelar peticiones, limpiar temporizadores, cerrar canales destruir() y el AbortController de 09-03

El ciclo de vida no es una curiosidad de los frameworks: es la respuesta al problema 4, el que te costó una lección entera. Y merece un matiz importante, porque es donde más gente se equivoca.

Un framework te garantiza que se llamará a la función de limpieza, pero no adivina qué hay que limpiar. Si abres un setInterval en el montaje y no devuelves nada en la limpieza, el intervalo sigue vivo exactamente igual que en JavaScript puro. Lo que el framework aporta es el momento: un sitio garantizado donde poner el apagado, invocado automáticamente cuando el componente desaparece. Eso convierte «acordarse de llamar a destruir() desde el sitio correcto» en «escribir el return de la función». Es una mejora enorme de ergonomía, no una supresión del problema.

  1. Librería frente a framework: qué significa «con opiniones»

La distinción clásica se resume en quién llama a quién:

  • Una librería es código que tú llamas. Tú diriges el flujo; la librería resuelve un problema concreto cuando se lo pides.
  • Un framework es código que te llama a ti. Él dirige el flujo; tú rellenas los huecos que él define. Es la llamada inversión de control.

En la práctica la frontera es porosa, pero la consecuencia sí es nítida: un framework decide cosas por ti. Y a esas decisiones ya tomadas se las llama «opiniones».

Decisión React (librería de vistas) Angular (framework completo)
Enrutador Eliges tú entre varios Incluido y oficial
Peticiones HTTP Eliges tú (fetch, axios, TanStack Query…) HttpClient incluido
Formularios Eliges tú Dos sistemas incluidos
Estado global Eliges tú (10-03) Servicios con señales, incluidos
Lenguaje JavaScript o TypeScript TypeScript, de hecho obligatorio
Estructura de carpetas La que quieras La que genera la CLI
Cadena de herramientas La montas tú (o con un meta-framework) La CLI lo hace todo

Ni una columna es mejor. Son intercambios distintos del mismo recurso escaso: decisiones.

  • Muchas opiniones: arranque rápido sin discutir, cualquier proyecto de la empresa se parece a los demás, la persona nueva se orienta en dos días. A cambio, cuando necesitas salirte del camino marcado, cuesta.
  • Pocas opiniones: máxima libertad, eliges la mejor herramienta para cada pieza. A cambio, cada equipo monta su propia combinación, y esa combinación hay que documentarla, mantenerla y actualizarla. Se le llama «fatiga de decisión» y es real: dos equipos de la misma empresa usando React pueden tener proyectos que no se parecen en nada.

Un dato para calibrar tu propio caso: Nómada Tareas es hoy un proyecto sin opiniones y con las tuyas. Tú decidiste Vite, Jest, Cypress, ESLint, la estructura de carpetas, el nombre de los eventos y el contrato de las vistas. Fue una decisión buena y coherente. También te llevó cuatro módulos.

  1. El coste real de adoptar un framework

Aquí es donde casi todos los materiales de aprendizaje callan. Un framework no es gratis. Estos son los cinco costes, con cifras aproximadas para que te hagas una idea del orden de magnitud (los números concretos envejecen; las proporciones no tanto).

Coste 1 · Peso descargado. Un motor de reconciliación o un sistema de inyección de dependencias son código que el usuario descarga y ejecuta antes de ver nada. Los paquetes base actuales, comprimidos, van desde unos pocos kilobytes en los frameworks compilados hasta varias decenas en los más completos. Tu aplicación entera pesa hoy 58,3 kB en tres peticiones. Para una aplicación grande, sumar el motor es irrelevante; para un widget en una página de marketing, puede duplicar el peso de la página.

Coste 2 · Curva de aprendizaje. No es la sintaxis, que se aprende en un día. Es el modelo mental: cuándo se vuelve a ejecutar tu función, por qué este efecto se dispara dos veces, qué es una dependencia estable, por qué esto no se actualiza. Cuenta entre dos semanas y dos meses hasta ser productivo de verdad, y bastante más hasta depurar con soltura un problema de reactividad.

Coste 3 · Cadena de herramientas. Un framework rara vez viene solo. Trae empaquetador, compilador, complementos del editor, reglas de ESLint específicas, un entorno de pruebas propio y, muy a menudo, TypeScript. Ese conjunto hay que instalarlo, configurarlo, actualizarlo y arreglarlo cuando se rompe. Es tiempo que no se dedica al producto y que no se ve en ninguna demostración.

Coste 4 · Dependencia. El código escrito para un framework no se puede llevar a otro sin reescribirlo. Tu Tablero, tu pedirJson, tus utilidades de fechas y formato son JavaScript puro: valen en cualquier sitio, hoy y dentro de diez años. Un componente escrito para un framework concreto vale mientras ese framework exista y mientras su API no cambie. Esta asimetría tiene una consecuencia de diseño muy práctica que conviene recordar: mantén la lógica de negocio fuera del framework. Que la vista sea del framework y el modelo sea tuyo. Nómada Tareas ya está así organizada, y no por casualidad.

Coste 5 · Rotación del ecosistema. Las bibliotecas que orbitan un framework popular cambian rápido: la recomendada para enrutar hace cinco años puede estar abandonada hoy. Actualizar un proyecto con veinte dependencias de ese ecosistema no es un fin de semana: es un proyecto. Este coste es proporcional a la popularidad, lo cual es una ironía útil de tener presente.

  1. Cuándo NO usar un framework

Una tabla de decisión honesta, del tipo que no suele aparecer en la portada de ninguna documentación.

Situación ¿Framework? Por qué
Página institucional con poca interactividad (un menú, un formulario de contacto) No El coste de descarga y de herramientas no compensa. HTML + un poco de JavaScript, o un generador de sitios estáticos
Widget que se incrusta en la web de un tercero No, o web components Un framework arrastra su motor y puede chocar con lo que ya haya en la página. Un web component nativo es una frontera limpia
Aplicación interna con formularios, tablas y permisos Es exactamente el caso para el que se diseñaron: mucha interfaz, mucho estado, mucha reutilización
Panel de datos con muchas vistas y navegación Enrutamiento, división de código y componentes reutilizables aportan desde el primer día
Equipo de una sola persona, proyecto pequeño y estable Probablemente no Las ventajas de convención y contratación no aplican; el coste sí
Proyecto que debe funcionar sin tocar durante diez años Con mucho cuidado El JavaScript estándar seguirá funcionando; una versión antigua de un framework con dependencias sin mantener es una deuda con intereses
Producto con SEO crítico y contenido mayormente estático Solo con renderizado en servidor, o mejor un enfoque de islas Verás por qué en 10-06
Interfaz con requisitos extremos de tamaño (kioscos, IoT, mercados con red muy limitada) Compilado o nada Cada kilobyte cuenta
Equipo que ya domina uno Sí, ese La productividad del equipo pesa más que cualquier comparativa técnica

Y la señal más fiable de todas, que puedes aplicar hoy mismo a Nómada Tareas: si estás escribiendo a mano, por segunda vez, algo que un framework te da hecho, es que el framework ya compensaba. Cuando escribiste reconciliar estabas en el límite. Cuando escribiste la lista virtualizada, ya lo habías cruzado —salvo que el objetivo, como aquí, fuera precisamente aprender cómo funciona por dentro.

  1. El panorama actual para situarse

Una tabla breve para ubicarte. No es una comparativa —esa es la lección 10-06—, es un mapa para que los nombres dejen de sonar a ruido.

Herramienta Qué es Reactividad Rasgo distintivo
React Librería de vistas DOM virtual (+ compilador emergente) Ecosistema mayor, máxima libertad, mayor demanda laboral
Vue Framework progresivo Señales + compilación Se adopta poco a poco; ecosistema oficial coherente
Angular Plataforma completa Señales (antes Zone.js) Todo incluido, TypeScript, inyección de dependencias
Svelte Compilador Compilación + runas El motor casi desaparece; muy poco código escrito
Solid Librería Señales de grano fino + compilación de JSX JSX con rendimiento de señales; sin DOM virtual
Astro Meta-framework de contenido Ninguna por defecto Islas: HTML estático con trozos interactivos, del framework que quieras
htmx Librería pequeña Ninguna El servidor devuelve HTML; el cliente lo inserta. Devuelve el estado al servidor
Web components Estándar del navegador Ninguna incluida Nativos, sin dependencias, duran lo que dure la plataforma

Dos observaciones sobre esta tabla. La primera: las tres últimas filas no son «alternativas menores», son enfoques distintos del problema. Astro y htmx cuestionan la premisa de que la interfaz deba construirse en el navegador. Si tu aplicación es sobre todo contenido con algo de interacción, esa premisa es cara y quizá innecesaria. Lo verás en 10-06.

La segunda: este módulo cubre React, Vue y Angular porque son los tres que concentran la mayoría del empleo, de la documentación y de los proyectos existentes, y porque entre los tres cubren los tres modelos de reactividad y los dos extremos del eje librería-framework. Quien entienda los tres, entiende los demás leyendo su documentación.

  1. Qué hace este módulo y qué no hace

Un aviso explícito, porque afecta a cómo debes leer las cinco lecciones siguientes.

El Módulo 11, el proyecto final, se construye en JavaScript puro. Con Nómada Tareas tal y como está: js/modelo/, js/datos/, js/vista/, sus 124 pruebas de Jest, sus tres recorridos de Cypress, su Vite y su service worker. No hay cambio de rumbo, no hay reescritura, y no vas a necesitar instalar React para terminar el curso.

Entonces, ¿para qué este módulo? Por tres razones concretas:

  1. Criterio. Vas a trabajar con frameworks, casi con seguridad. La diferencia entre usarlos bien y usarlos por inercia es saber qué problema resuelven. Esa es la razón principal.
  2. Perspectiva sobre tu propio código. Ver tu reconciliar convertido en una propiedad key, tu destruir() en una función de limpieza y tu caché por versión en un computed ilumina hacia atrás lo que has construido. Se entiende mejor lo propio cuando se ve nombrado por otros.
  3. Decisión informada. Al terminar 10-06 habrás visto la misma pantalla escrita de cuatro formas, con sus métricas. Que el proyecto final sea JavaScript puro dejará de ser lo que sabes hacer para pasar a ser lo que has decidido, que no es lo mismo.

Y hay una razón práctica más: aprender un framework de verdad requiere un curso propio. Lo que sí puedes hacer en cinco lecciones es entender los cuatro enfoques lo bastante bien como para elegir cuál aprender a fondo, y para leer código ajeno sin sentirte perdido. Ese es el objetivo declarado.

Errores Comunes y Consejos

Creer que un framework hace la aplicación más rápida. No es cierto en general. Añade peso al arranque y una capa de trabajo en cada actualización. Lo que hace es que sea difícil escribir una interfaz lenta por descuido, porque la reconciliación evita el redibujado completo que la mayoría escribiría a mano. Tú ya no eres esa mayoría: tu render() tarda 31 ms medidos. Compara siempre contra lo que tienes, no contra un supuesto.

Elegir framework por comparativas de rendimiento. Las diferencias entre los grandes, en aplicaciones reales, son de milisegundos y quedan sepultadas por decisiones que sí importan: cuántos datos pides, cuántas imágenes cargas, cómo despliegas. Elegir por benchmarks de listas de 10.000 filas es optimizar una fila que nunca vas a tener.

Adoptar uno para un problema que no tienes. Si tu página tiene un menú desplegable y un formulario, el framework es el problema, no la solución. La pregunta no es «¿es bueno?», sino «¿qué me está resolviendo hoy?».

Meterlo todo en el estado global desde el primer día. El error más caro y el más frecuente. Empieza local, eleva cuando dos hermanos lo necesiten, y usa un almacén compartido solo cuando el árbol te obligue. Lo verás con detalle en 10-03.

Mezclar estado de servidor y estado de interfaz. Guardar la respuesta de listarTareas() en el mismo sitio donde guardas «el modal está abierto» es meter dos problemas con reglas distintas en la misma caja. Consecuencia típica: datos obsoletos que nadie sabe cuándo refrescar.

Meter la lógica de negocio en los componentes. Es el fallo que más caro sale a medio plazo, porque es el que hace irreversible la elección. Tus reglas R1–R10, tu Tablero, tu pedirJson: fuera del framework. El componente pinta y recoge eventos; el modelo decide. Con esa disciplina, cambiar de framework es reescribir la vista; sin ella, es reescribir la aplicación.

Creer que el ciclo de vida limpia por ti. Te da el sitio y el momento, no el contenido. Un setInterval sin cancelar sigue siendo una fuga con framework y sin él. Todo lo que aprendiste en 09-03 sigue vigente.

Consejo: aprende uno de verdad antes que tres por encima. El modelo mental de la reactividad se transfiere; la sintaxis, no tanto. Quien domina uno lee los otros con relativa comodidad. Quien ha hecho el tutorial de los tres no domina ninguno.

Consejo: prototipa un día con tu caso más difícil. No con la lista de tareas de la documentación: con lo peor que tengas. Para Nómada Tareas sería la lista de 600 tareas con filtro en tiempo real y actualizaciones por WebSocket. Un día de prototipo enseña más que un mes de comparativas, y es el consejo que retomarás en 10-06.

Ejercicios

Ejercicio 1 · El diagnóstico de tu propio código

Abre mentalmente (o de verdad, si lo tienes escrito) js/vista/tablero-vista.js y js/vista/controlador.js. Para cada uno de los ocho problemas del apartado 2, responde:

  1. ¿Lo tienes resuelto, parcialmente resuelto o sin resolver?
  2. ¿Cuántas líneas de tu código están dedicadas a resolverlo?
  3. Si mañana Nómada Tareas creciera a cinco pantallas y treinta tipos de componente, ¿ese problema se volvería más caro, igual o más barato?

Escribe la respuesta en una tabla de tres columnas. El objetivo no es la exactitud de los números, sino identificar qué problemas escalan mal.

Ejercicio 2 · De imperativo a declarativo

Este código imperativo gestiona el filtro por responsable de Nómada Tareas. Reescríbelo en estilo declarativo y explica qué has ganado.

// Imperativo
function filtrarPorResponsable(nombre) {
  const filas = document.querySelectorAll('.tarea');
  let visibles = 0;
  let horas = 0;

  for (const fila of filas) {
    const coincide = nombre === null || fila.dataset.responsable === nombre;
    fila.hidden = !coincide;
    if (coincide) {
      visibles++;
      horas += Number(fila.dataset.horas);
    }
  }

  document.querySelector('#contador').textContent = `${visibles} tareas`;
  document.querySelector('#horas').textContent = `${horas} h`;
  document.querySelector('#vacio').hidden = visibles > 0;
  document.querySelector('#filtro-activo').textContent = nombre ?? 'Todos';
}

Pistas: fíjate en de dónde salen los datos (¿del DOM o del modelo?), en cuántos sitios se actualizan y en qué pasa si una tarea cambia de responsable mientras el filtro está activo.

Ejercicio 3 · La decisión honesta

Para cada uno de estos tres proyectos, decide si usarías un framework y cuál de los tres enfoques del apartado 20 encaja mejor. Justifica con al menos tres criterios de esta lección y nombra explícitamente el coste que estás aceptando.

  • (a) La web pública de Taller Nómada: quiénes somos, tarifas, galería de fotos, formulario de contacto y un calendario de disponibilidad que se actualiza cada hora. Debe posicionar bien en buscadores. Una persona la mantiene tres horas al mes.
  • (b) Nómada Tareas convertida en producto para veinte talleres: cinco pantallas, permisos por rol, informes, edición colaborativa en tiempo real, un equipo de cuatro personas y un horizonte de cinco años.
  • (c) Un widget de «reserva tu plaza» que Taller Nómada quiere ofrecer a otras webs para que lo incrusten con dos líneas de código. Esas webs usan WordPress, Shopify y cosas peores.

Soluciones

Solución 1

Tu tabla debería parecerse bastante a esta. Los números de líneas son aproximados y lo importante es la última columna:

# Problema Estado en tu código Líneas aprox. ¿Escala?
1 Sincronizar estado y pantalla Resuelto: ciclo estado → render ~40 (render + actualizar) , escala bien: el ciclo no crece con la aplicación
2 Identidad de nodos Resuelto: reconciliar con data-id ~20 Mal: solo vale para hijos directos con clave. Con componentes anidados habría que rehacerlo
3 Rendimiento Resuelto: índice, caché, lotes, virtualización ~200 repartidas Mal: cada pantalla nueva necesita su propia virtualización y su propia caché
4 Limpieza Resuelto: destruir() + AbortController ~30 Mal: depende de que alguien llame a destruir(). Con veinte componentes, el olvido es cuestión de tiempo
5 Composición Parcial: funciones que pintan, sin estado por instancia Mal: no hay sitio natural para el estado local de una tarjeta
6 Comunicación entre partes lejanas Parcial: CustomEvent ~25 Mal: el flujo solo se sigue buscando cadenas por el proyecto
7 Estructura/estilo/comportamiento Sin resolver: cuatro ficheros por tarjeta Mal: cuatro acoplamientos sin comprobar, por componente
8 Convenciones Parcial: existen, no están escritas Mal: cada persona nueva las aprende leyendo código

Conclusión del ejercicio: el problema 1 lo tienes resuelto de forma que escala; los demás, no. Ese es, en una frase, el argumento entero a favor de los frameworks para aplicaciones que crecen — y el argumento en contra para aplicaciones que no van a crecer.

Solución 2

Lo primero es diagnosticar los tres defectos del código imperativo:

  1. Los datos salen del DOM (fila.dataset.horas, fila.dataset.responsable). El DOM se ha convertido en la base de datos, y es una mala base de datos: todo es texto, no hay validación, y cualquier cosa que toque el HTML corrompe los cálculos.
  2. Hay cuatro puntos de actualización (#contador, #horas, #vacio, #filtro-activo) que hay que recordar. Añadir un quinto dato visible obliga a tocar esta función y todas las demás que cambien el filtro.
  3. Usa hidden en lugar de no renderizar. Los nodos ocultos siguen existiendo, ocupan memoria, aparecen en el árbol de accesibilidad si se hace mal y se encuentran con Ctrl+F. Con 600 tareas, hay 600 nodos por 6 visibles.

La versión declarativa:

// Declarativo: el filtro es estado; la pantalla es una consecuencia
function filtrarPorResponsable(nombre) {
  estado.filtros.responsable = nombre;   // 1 · cambia el estado
  render();                              // 2 · vuelve a describir la pantalla
}

function render() {
  const visibles = tareasVisibles(estado);      // del MODELO, no del DOM

  reconciliar(lista, visibles, (t) => t.id, (t, nodo) => pintarTarjeta(t, nodo, HOY));

  const horas = visibles.reduce((s, t) => s + t.horasEstimadas, 0);

  $('#contador').textContent       = `${visibles.length} tareas`;
  $('#horas').textContent          = `${horas} h`;
  $('#vacio').hidden               = visibles.length > 0;
  $('#filtro-activo').textContent  = estado.filtros.responsable ?? 'Todos';
}

Qué has ganado, concretamente:

  • Una sola fuente de verdad. Los números salen de los objetos Tarea, con sus tipos correctos. Si el HTML cambia, los cálculos siguen siendo correctos.
  • Un solo sitio que actualizar. Añadir «esfuerzo ponderado» al panel es una línea en render(), y funciona para todas las acciones existentes, no solo para el filtro.
  • Corrección ante cambios concurrentes. Si el CanalTablero de 07-04 cambia el responsable de una tarea mientras el filtro está activo, la versión imperativa deja esa fila visible con el responsable equivocado hasta que alguien vuelva a filtrar. La declarativa la recoloca en el siguiente render(), porque no hay «memoria» de lo ya calculado.
  • Menos nodos. reconciliar elimina de verdad las tarjetas que no coinciden, en lugar de ocultarlas: es la diferencia entre 1.194 nodos y 7.812 que mediste en 09-04.

Lo que has perdido, para ser justos: el filtro imperativo solo toca la propiedad hidden de las filas, mientras que el declarativo recorre las tareas, filtra, ordena y reconcilia. Con seis tareas es indistinguible; el modelo declarativo siempre hace más trabajo por actualización, y a cambio hace ese trabajo bien y una sola vez escrito.

Solución 3

(a) La web pública de Taller Nómada: sin framework de cliente.

Criterios: la interactividad es mínima (un menú, un formulario, un calendario que se refresca cada hora); el SEO es crítico y el HTML debe llegar hecho desde el servidor; el mantenimiento es de tres horas al mes, lo que hace que la rotación del ecosistema (coste 5) sea el riesgo dominante — nadie va a estar ahí para actualizar veinte dependencias.

Enfoque adecuado: un generador de sitios estáticos o un enfoque de islas al estilo de Astro, con HTML estático y un solo trozo interactivo para el calendario. Un poco de JavaScript suelto para el menú y el formulario basta.

Coste aceptado: si dentro de dos años quieren añadir una zona privada con reservas y perfil de usuario, habrá que replantear. Es un coste razonable frente a mantener una cadena de herramientas para un formulario de contacto.

(b) Nómada Tareas como producto: framework, sin duda.

Criterios: cinco pantallas con navegación y división de código; cuatro personas que necesitan convenciones compartidas y contratos explícitos (problemas 7 y 8); mucho estado compartido entre pantallas (permisos, usuario, filtros); edición colaborativa, que multiplica los problemas de estado de servidor del apartado 15; y un horizonte de cinco años, en el que la contratación y la formación pesan tanto como el código.

Cuál: cualquiera de los tres, y la elección debe basarse en el equipo y el mercado local antes que en la técnica. Si el equipo ya conoce uno, ese. Si viene de otras plataformas con inyección de dependencias y tipos, Angular encaja bien; si se valora libertad y mercado laboral amplio, React; si se valora una curva suave y un ecosistema oficial coherente, Vue. Es la decisión de 10-06.

Coste aceptado: dos meses de curva de aprendizaje repartidos entre cuatro personas, una cadena de herramientas que mantener, y una dependencia real. Se mitiga con la disciplina del apartado 18: Tablero, reglas R1–R10 y pedirJson se quedan en JavaScript puro, fuera del framework.

(c) El widget incrustable: web components nativos, o JavaScript puro.

Criterios: se ejecuta en páginas ajenas cuyo CSS y cuyo JavaScript no controlas; el peso importa mucho porque se suma a una página que no es tuya; el aislamiento de estilos es un requisito, no un lujo (el Shadow DOM lo da de verdad); y la longevidad importa, porque no puedes pedirle a cien webs que actualicen tu script.

Enfoque adecuado: un custom element nativo con Shadow DOM, sin dependencias, o —si hace falta más interfaz— un framework compilado que deje un paquete muy pequeño y pueda empaquetarse como custom element.

Coste aceptado: escribir más a mano, sin las comodidades de un framework, y resolver tú los ocho problemas del apartado 2 dentro del widget. Es aceptable porque el widget es pequeño y su superficie está acotada: precisamente el caso donde el framework no compensa.

Conclusión

Has hecho el diagnóstico antes de mirar ningún remedio, que es la única forma de que los remedios se entiendan. Conoces los ocho problemas que aparecen en cualquier interfaz que muestre datos que cambian —sincronización, identidad de los nodos, rendimiento del redibujado, limpieza, composición, comunicación entre partes lejanas, acoplamiento entre estructura, estilo y comportamiento, y convenciones de equipo— y sabes exactamente cuál es tu solución para cada uno, cuánto te costó y cuáles de ellas escalan mal cuando el proyecto crece.

Entiendes el salto de imperativo a declarativo no como una cuestión de elegancia sino de aritmética: escribir n descripciones de estado en lugar de n×(n−1) transiciones. Tienes la ecuación UI = f(estado) con sus cuatro consecuencias —la pantalla se vuelve razonable, comprobable, el DOM deja de ser la fuente de la verdad, y aparece la factura de rendimiento que hay que pagar con reconciliación—. Y has visto el mismo marcado de una tarea como hecha escrito de las dos formas: doce pasos frágiles frente a dos pasos y una descripción.

Sabes qué significa reactividad con precisión —una dependencia declarada que el sistema mantiene solo— y conoces las tres familias con sus mecanismos reales: el DOM virtual que compara dos árboles y por eso necesita una key que es literalmente tu data-id, con un coste proporcional al tamaño del árbol; las señales, que registran quién lee qué y notifican al escribir, con un coste proporcional a lo que cambia y un modelo mental más sutil donde la reactividad se pierde al desestructurar; y la compilación, que resuelve el problema antes de llegar al navegador, con paquetes mínimos a cambio de escribir en un lenguaje que solo entiende su compilador. Ninguna es la correcta: cada una optimiza algo distinto, y las tres se están mezclando.

Sabes qué es un componente —plantilla, estado, comportamiento y estilos en una unidad instanciable, componible y aislada— y por qué es mejor unidad que tus módulos de vista/, con sus cuatro acoplamientos sin comprobar entre el <template>, el CSS, tarjeta.js y controlador.js, sin sitio natural para el estado por instancia y sin aviso cuando reconciliar elimina un nodo. Distingues los cuatro tipos de estado —local, elevado, compartido y de servidor— con la regla de empezar siempre local y subir solo cuando obliguen, y entiendes por qué el estado del servidor es un problema de otra naturaleza: no eres su fuente de verdad, puede quedar obsoleto sin avisarte y tiene estados intermedios que el estado local no tiene. Y sabes que el ciclo de vida existe para resolver el problema 4, dándote el momento garantizado para apagar lo que encendiste, sin adivinar por ti qué hay que apagar.

Tienes clara la diferencia entre librería y framework —quién llama a quién— y qué significa «con opiniones»: decisiones ya tomadas, que ahorran discusiones y cuestan libertad. Y, sobre todo, conoces el coste real de adoptar uno: peso descargado, dos semanas a dos meses de curva, una cadena de herramientas que mantener, una dependencia que solo se mitiga sacando la lógica de negocio fuera del framework, y una rotación del ecosistema proporcional a la popularidad. Con su contrapartida: la tabla de cuándo NO usar uno, con la señal más fiable de todas —si estás escribiendo a mano por segunda vez algo que un framework te da hecho, ya compensaba.

Queda dicho de forma explícita: el Módulo 11 se construye en JavaScript puro, con Nómada Tareas tal y como está, sus 124 pruebas y sus tres recorridos de Cypress. Este módulo no es un cambio de tecnología sino de perspectiva. En las cuatro lecciones siguientes vas a ver la misma pantalla —la lista de tareas con su filtro por responsable y su botón de marcar como hecha— reimplementada cuatro veces, para que la comparación sea real y no una lista de características. Empezamos por el enfoque del DOM virtual y por la librería con el ecosistema más grande: Introducción a React.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados