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
- El diagnóstico antes del remedio
- Los ocho problemas de construir una interfaz a mano
- La tabla del diagnóstico: problema → tu solución → cómo lo resuelve un framework
- Imperativo frente a declarativo
UI = f(estado): la ecuación y sus consecuencias- El mismo trozo de tablero, escrito de las dos formas
- Qué significa «reactividad» exactamente
- Familia 1: DOM virtual y reconciliación
- Familia 2: reactividad de grano fino con señales
- Familia 3: compilación en tiempo de construcción
- Las tres familias en una tabla
- El componente: plantilla, estado, comportamiento y estilos
- Por qué el componente es mejor unidad que tus módulos de
vista/ - Estado: local, elevado, compartido y de servidor
- Por qué el estado del servidor es un problema distinto
- El ciclo de vida y por qué existe
- Librería frente a framework: qué significa «con opiniones»
- El coste real de adoptar un framework
- Cuándo NO usar un framework
- El panorama actual para situarse
- Qué hace este módulo y qué no hace
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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.
- 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.
- 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:
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
UI = f(estado): la ecuación y sus consecuencias
UI = f(estado): la ecuación y sus consecuenciasLa forma condensada de todo lo anterior es una ecuación que verás en la documentación de casi todos los frameworks modernos:
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:
- 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.
- La pantalla se vuelve comprobable. Si
fes 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. - 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. - Vuelve el problema del rendimiento. Si
fproduce 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.
- 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
- 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:
- Estado → estado derivado: si cambian las tareas, cambian las horas abiertas, el número de pendientes y la lista filtrada.
- 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.
- 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.memoy 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.
- 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
setTimeoutno 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,
useMemotiene menos equivalentes necesarios en el mundo de las señales.
- 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.
- 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) |
Sí |
| 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.
- 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:
- 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 undataseto en unMapexterno. - 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. - Aísla. Los estilos con ámbito propio impiden que la clase
.titulode la tarjeta afecte a la.titulode 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.
- Por qué el componente es mejor unidad que tus módulos de
vista/
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 atributoCuatro 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.
- 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?
- 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.
- 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.
- 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.
- 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.
- 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 | Sí | 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 | Sí | 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.
- 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.
- 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:
- 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.
- Perspectiva sobre tu propio código. Ver tu
reconciliarconvertido en una propiedadkey, tudestruir()en una función de limpieza y tu caché por versión en uncomputedilumina hacia atrás lo que has construido. Se entiende mejor lo propio cuando se ve nombrado por otros. - 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:
- ¿Lo tienes resuelto, parcialmente resuelto o sin resolver?
- ¿Cuántas líneas de tu código están dedicadas a resolverlo?
- 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) |
Sí, 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:
- 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. - 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. - Usa
hiddenen 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
CanalTablerode 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 siguienterender(), porque no hay «memoria» de lo ya calculado. - Menos nodos.
reconciliarelimina 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
- ¿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
