Las tres lecciones anteriores han cubierto pruebas, accesibilidad y rendimiento, cada una como un ángulo de calidad transversal aplicado sobre TaskFlow tal como quedó al final del módulo 8. Esta última lección del módulo cierra con una síntesis distinta: en lugar de un ángulo nuevo, recorre de principio a fin las decisiones de diseño ya tomadas durante el curso, las agrupa en pares de patrón recomendado frente a anti-patrón, y explica por qué cada anti-patrón, aunque a veces "funcione" a corto plazo, termina costando más de lo que ahorra. Es, en cierto sentido, un repaso del curso completo visto desde el reverso: no qué hace cada pieza de Lit, sino qué error concreto se evita al usarla bien.

Contenido

  1. Por qué conviene una lección de síntesis antes del proyecto final
  2. Tabla general: patrones frente a anti-patrones del curso
  3. Inmutabilidad frente a mutar arrays y objetos en el sitio
  4. composed: true frente a eventos personalizados que no salen del Shadow DOM
  5. Plantillas declarativas frente a lógica de negocio dentro de render()
  6. Componentes descompuestos frente a componentes que acumulan responsabilidades
  7. Limpieza de recursos frente a fugas en disconnectedCallback y controladores
  8. Un criterio final: preguntas que detectan un anti-patrón antes de escribirlo
  9. Hacia el proyecto final: repasar TaskFlow de principio a fin

  1. Por qué conviene una lección de síntesis antes del proyecto final

Cada anti-patrón de esta lección ya apareció, en algún momento del curso, señalado como "error común" al final de una lección concreta, en el contexto estrecho de la técnica que se estaba explicando en ese momento. Lo que esa forma de presentarlos no podía mostrar es el patrón que se repite entre módulos distintos: que olvidar composed: true (módulo 5) y no limpiar un setInterval en disconnectedCallback (módulo 6) son, en el fondo, la misma clase de error —olvidar la mitad de una operación que exige simetría—, aunque aparezcan en dos lecciones separadas por varios módulos. Verlos juntos, ahora que ya se conoce el curso completo, ayuda a reconocerlos con más facilidad en código nuevo, incluido el que se va a escribir en el módulo 10.

  1. Tabla general: patrones frente a anti-patrones del curso

# Anti-patrón Patrón recomendado Dónde se explicó
1 Mutar un array o un objeto en el sitio dentro de una propiedad reactiva Reemplazar la propiedad por una copia nueva (map, filter, {...obj}) Lección "Comunicación de Padre a Hijo con Propiedades" (módulo 5)
2 Despachar un evento personalizado sin composed: true desde un componente con Shadow DOM bubbles: true y composed: true juntos, para que el evento salga del shadow root Lección "Eventos Personalizados: Comunicación de Hijo a Padre" (módulo 5)
3 Meter lógica de negocio o cálculos costosos directamente dentro de render() Derivar ese cálculo en willUpdate, o extraerlo a un método/controlador aparte Lecciones "Hooks Reactivos" (módulo 6) y "Rendimiento y Optimización" (módulo 9)
4 Un único componente que acumula demasiadas responsabilidades sin descomponerse Extraer sub-componentes con una responsabilidad clara, como <user-avatar> o <task-filter> Lecciones "Slots y Estilizado de Contenido Distribuido" (módulo 4) y "Contexto Compartido con @lit/context" (módulo 7)
5 No liberar recursos (temporizadores, suscripciones) en disconnectedCallback o en hostDisconnected Limpiar en el mismo lugar simétrico donde se arrancó el recurso Lecciones "Callbacks del Ciclo de Vida" y "Controladores Reactivos" (módulo 6)

Las cinco filas comparten una misma estructura de fondo, aunque a primera vista parezcan problemas distintos: cada anti-patrón "ahorra" una línea o un paso en el momento de escribirlo, y cada patrón recomendado invierte ese pequeño coste inicial a cambio de un comportamiento predecible más adelante, cuando el componente crece, se reutiliza en un contexto distinto, o convive con otros que no controla directamente.

  1. Inmutabilidad frente a mutar arrays y objetos en el sitio

La lección "Comunicación de Padre a Hijo con Propiedades" explicó, con la tabla de operaciones de la lección 05-03, por qué this.tareas.push(...) o this.tareas[0].estado = 'hecha' no disparan ninguna actualización visible en Lit: la comparación por defecto de una propiedad reactiva de tipo objeto o array es por referencia, no por contenido, y una mutación en el sitio nunca cambia esa referencia.

// Anti-patrón: muta el array existente
gestionarTareaCambiada(idTarea, event) {
  const tarea = this.tareas.find((t) => t.id === idTarea);
  tarea.estado = event.detail.nuevoEstado;
  this.requestUpdate(); // parche que oculta el problema real
}

// Patrón recomendado: reemplaza con una copia nueva
gestionarTareaCambiada(idTarea, event) {
  const nuevoEstado = event.detail.nuevoEstado;
  this.tareas = this.tareas.map((tarea) =>
    tarea.id === idTarea ? { ...tarea, estado: nuevoEstado } : tarea
  );
}

El this.requestUpdate() forzado en la primera versión no es una corrección, sino un síntoma: "funciona" porque obliga a Lit a volver a renderizar sin importar si detectó o no un cambio real, pero no resuelve el problema de fondo, y cualquier otro código que dependiera de comparar el array anterior con el actual (una herramienta de depuración con historial, o una futura funcionalidad de "deshacer") seguiría fallando, porque los objetos mutados en el sitio no dejan ningún rastro de cuál era su valor anterior. La versión inmutable no necesita ningún requestUpdate() manual porque la propia asignación this.tareas = ... ya cambia la referencia, que es justo lo que el sistema de reactividad de Lit necesita detectar por sí solo.

  1. composed: true frente a eventos personalizados que no salen del Shadow DOM

La lección "Eventos Personalizados: Comunicación de Hijo a Padre" señaló este error como el más frecuente al depurar comunicación entre componentes, y merece repetirse aquí junto al resto porque comparte la misma naturaleza que el anti-patrón del apartado anterior: una operación que parece completa, pero le falta una segunda mitad imprescindible.

// Anti-patrón: el evento nunca sale del shadow root de <task-card>
this.dispatchEvent(new CustomEvent('tarea-cambiada', { detail: { nuevoEstado } }));

// Patrón recomendado: bubbles y composed juntos
this.dispatchEvent(
  new CustomEvent('tarea-cambiada', {
    detail: { nuevoEstado },
    bubbles: true,
    composed: true,
  })
);

Sin composed: true, el código de la primera versión no lanza ningún error ni ninguna advertencia: el evento se crea, se despacha, y cualquier console.log colocado dentro del propio método confirmaría, engañosamente, que "todo funciona". El fallo solo se manifiesta un paso más allá, en <task-list>, que jamás llega a recibir el evento porque este queda atrapado dentro del shadow root de <task-card>, exactamente la clase de fallo silencioso —sin mensaje de error, sin traza en la consola— que resulta más costosa de diagnosticar que un error explícito.

  1. Plantillas declarativas frente a lógica de negocio dentro de render()

La lección "Rendimiento y Optimización" (09-03) ya explicó el coste de recalcular dentro de render() algo que podría derivarse una sola vez en willUpdate; este apartado retoma la misma idea desde un ángulo distinto, el de la claridad del propio código, no solo su coste:

// Anti-patrón: decide, calcula y formatea todo dentro de render()
render() {
  const diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
  const claseUrgencia = diasRestantes < 1 ? 'critico' : diasRestantes < 3 ? 'aviso' : 'normal';
  return html`<p class="${claseUrgencia}">${diasRestantes} días restantes</p>`;
}

// Patrón recomendado: render() solo traduce datos ya preparados a HTML
willUpdate(changedProperties) {
  if (changedProperties.has('fechaLimite')) {
    this._diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
    this._claseUrgencia = this._diasRestantes < 1 ? 'critico' : this._diasRestantes < 3 ? 'aviso' : 'normal';
  }
}

render() {
  return html`<p class="${this._claseUrgencia}">${this._diasRestantes} días restantes</p>`;
}

Más allá del coste de recalcular en cada renderizado (ya cubierto en la lección anterior), la primera versión mezcla dos responsabilidades distintas dentro de un mismo método: decidir qué significa el dato (¿cuántos días quedan, y a partir de cuántos días algo se considera "crítico"?) y decidir cómo mostrarlo (qué etiqueta HTML, qué clase CSS). La segunda versión separa ambas preguntas en dos lugares distintos del ciclo de vida, cada uno con una responsabilidad clara: willUpdate decide el significado, render() decide la representación. Esta separación, aunque parezca un simple reordenamiento del mismo código, facilita razonar sobre cada mitad por separado —y, no por casualidad, es exactamente la misma separación que ya se explicó, con otro ejemplo, en la lección "Hooks Reactivos" del módulo 6.

  1. Componentes descompuestos frente a componentes que acumulan responsabilidades

<task-card>, a lo largo del curso, podría haber crecido como una única clase monolítica que dibujara directamente, dentro de su propio render(), tanto el avatar de la persona asignada como el filtro de búsqueda de toda la aplicación. En su lugar, el curso ha ido extrayendo, en el momento en que cada responsabilidad se volvía suficientemente distinta, un componente propio: <user-avatar> en el módulo 4, cuando mostrar una persona asignada (con imagen o iniciales, con su propio <slot> y sus propias reglas de estilo) dejó de ser un simple detalle visual de <task-card> y pasó a merecer su propia clase reutilizable; <task-filter> en el módulo 7, cuando gestionar el filtro de búsqueda dejó de ser algo que <task-list> pudiera seguir absorbiendo sin mezclar dos responsabilidades ajenas entre sí (mostrar tareas, por un lado; decidir cuáles mostrar, por otro).

Señal de que conviene extraer un componente Ejemplo ya aplicado en TaskFlow
Una pieza de la plantilla tiene su propio ciclo de estilos, independiente del resto <user-avatar>, con su propia variable CSS --tamano-avatar
Una pieza de comportamiento podría reutilizarse en un contexto distinto al actual <user-avatar>, pensado para poder aparecer en cualquier lugar de TaskFlow que necesite mostrar una persona, no solo dentro de <task-card>
Dos responsabilidades dentro de la misma clase cambian por motivos distintos y en momentos distintos <task-filter> cambia cuando cambian las reglas de filtrado; <task-list> cambia cuando cambia cómo se muestran las tareas
El nombre de una clase empieza a necesitar la conjunción "y" para describirse ("la lista que también filtra y también pagina") Evitado extrayendo <task-filter> en lugar de absorber el filtro dentro de <task-list>

El anti-patrón simétrico a evitar es el contrario: extraer un componente nuevo por cada detalle mínimo, sin que exista ninguna de las señales de la tabla anterior, multiplicando el número de ficheros y de Shadow DOMs sin ninguna ganancia real de claridad ni de reutilización. El criterio, igual que con el resto de esta lección, no es "descomponer siempre" ni "descomponer nunca", sino reconocer las señales concretas que justifican la extracción, tal como TaskFlow lo ha hecho en los dos casos de la tabla.

  1. Limpieza de recursos frente a fugas en disconnectedCallback y controladores

El último par de esta lección conecta directamente los módulos 6 y 9: la lección "Callbacks del Ciclo de Vida" advirtió sobre no limpiar un setInterval en disconnectedCallback, y la lección "Controladores Reactivos" repitió la misma advertencia, ahora sobre hostDisconnected, señalando que el riesgo se multiplica por cada host que use el controlador.

// Anti-patrón: arranca el temporizador, nunca lo detiene
connectedCallback() {
  super.connectedCallback();
  this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}
// (sin disconnectedCallback, o con uno que no llama a clearInterval)

// Patrón recomendado: simetría entre conexión y desconexión
connectedCallback() {
  super.connectedCallback();
  this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}

disconnectedCallback() {
  clearInterval(this._idIntervalo);
  super.disconnectedCallback();
}

Una <task-card> que se elimina del DOM (por ejemplo, porque el filtro de <task-filter> la excluye del resultado) sin que su temporizador se detenga sigue ejecutando _comprobar() cada minuto, indefinidamente, aunque ya no exista ningún nodo visible al que esa comprobación pueda afectar; con cientos de tarjetas creadas y destruidas a lo largo de una sesión larga de uso (el escenario de una lista grande explorado en la lección anterior), esos temporizadores huérfanos se acumulan uno tras otro, consumiendo memoria y ciclos de proceso sin ningún beneficio, un problema que solo se manifiesta con el tiempo y que rara vez aparece en una prueba manual rápida de cinco minutos, precisamente el tipo de fallo que una batería de tests automatizados como la de la lección 09-01 tampoco detecta si no se diseña específicamente para comprobarlo.

  1. Un criterio final: preguntas que detectan un anti-patrón antes de escribirlo

Los cinco pares anteriores comparten un rasgo común que conviene convertir en hábito, no solo en una lista para consultar después de escribir el código: cada anti-patrón corresponde a una pregunta concreta que, formulada a tiempo, lo habría evitado.

Pregunta que conviene hacerse Anti-patrón que detecta
¿Estoy reemplazando esta propiedad por un valor nuevo, o modificando el valor que ya tenía? Mutación en el sitio (apartado 3)
Si este componente tiene Shadow DOM, ¿este evento necesita salir de él? Falta de composed: true (apartado 4)
¿Este cálculo depende solo de propiedades que ya cambiaron, o se repite sin condición? Lógica de negocio en render() (apartado 5)
¿Esta clase cambia por más de un motivo distinto? Responsabilidades sin descomponer (apartado 6)
Por cada recurso que arranco, ¿dónde exactamente lo voy a detener? Fuga de recursos (apartado 7)

  1. Hacia el proyecto final: repasar TaskFlow de principio a fin

Con esta síntesis, el módulo 9 completa la cobertura de calidad que TaskFlow necesitaba antes de considerarse una aplicación acabada: pruebas automatizadas que verifican su comportamiento sin depender de comprobaciones manuales, accesibilidad para quien no usa un ratón sobre una pantalla convencional, rendimiento revisado con criterio frente a volúmenes de datos reales, y ahora un catálogo de patrones y anti-patrones que resume, de un vistazo, las decisiones de diseño de los ocho módulos anteriores.

Lo que queda pendiente no es ningún concepto nuevo de Lit —el curso ya ha cubierto plantillas, reactividad, estilos, eventos, ciclo de vida, directivas, contexto, integración, pruebas y buenas prácticas—, sino recorrer TaskFlow de principio a fin como aplicación completa: repasar cada componente construido a lo largo del curso, identificar qué piezas quedaron mencionadas pero no completamente resueltas, y terminar de ensamblarlas en la versión final y acabada de la aplicación. Esa es, precisamente, la tarea de la última lección del curso, "Proyecto: Construyendo TaskFlow", el módulo de cierre que da por terminado el recorrido completo desde el primer componente Lit de la lección 01-03 hasta la aplicación final.

Errores Comunes y Consejos

  • Tratar esta lección como una lista de reglas aisladas, en lugar de un criterio transferible: los cinco pares del apartado 2 no son una lista cerrada de errores específicos de TaskFlow, sino ejemplos concretos de un puñado de preguntas generales (apartado 8) que se pueden aplicar a cualquier componente Lit futuro, incluido cualquiera que se añada en el proyecto final.
  • Aplicar un patrón recomendado sin entender qué anti-patrón evita: copiar bubbles: true, composed: true en todos los eventos "por costumbre", sin saber que resuelve el problema concreto de atravesar el Shadow DOM, deja a quien escribe el código sin criterio para decidir cuándo, alguna vez, ninguna de las dos opciones haría falta (un evento que un componente solo necesita escuchar sobre sí mismo, sin ningún padre interesado).
  • Confundir "descomponer en componentes" con una regla numérica: como se ha señalado en el apartado 6, no existe un número correcto de componentes por aplicación; las señales de la tabla del apartado 6, no un recuento arbitrario, son el criterio que debería decidir cuándo extraer una pieza nueva.
  • Revisar los anti-patrones solo al final de un proyecto, no durante su desarrollo: esta lección llega, deliberadamente, antes del proyecto final, no después; el objetivo es que las preguntas del apartado 8 se conviertan en hábito mientras se construye el módulo 10, no en una lista de comprobación aplicada a toda prisa una vez terminado.

Ejercicios

  1. Repasa el render() de <task-board> tal como quedó tras la lección "Mixins y Composición de Comportamiento" del módulo 6 (con ConEstadoCarga) y señala, con el criterio del apartado 5 de esta lección, si this.conIndicadorDeCarga(...) cuenta como "lógica de negocio dentro de render()" o si encaja mejor en el patrón recomendado. Justifica la respuesta.
  2. Un compañero de equipo, revisando <task-filter>, encuentra que manejarTexto y manejarEstado llaman a this.valorActual.actualizar({...}), que a su vez reasigna this._filtroProvider.value en <task-board> con el operador de propagación ({ ...this._filtroProvider.value, ...cambios }, visto en la lección "Contexto Compartido con @lit/context"). Explica, con el criterio del apartado 3, por qué esa reasignación cuenta como el patrón recomendado y no como el anti-patrón de mutación en el sitio.
  3. Aplicando las cinco preguntas del apartado 8, identifica cuál de ellas habría señalado el problema si ContadorTiempoRestanteController (módulo 6) hubiera asignado directamente this.host.cercaDeVencer = ... en lugar de mantener su propio campo cercaDeVencer y llamar a this.host.requestUpdate(), tal como se advirtió en los "Errores Comunes" de la lección "Controladores Reactivos".

Soluciones

  1. this.conIndicadorDeCarga(...) no es lógica de negocio dentro de render() en el sentido señalado por el apartado 5: no calcula ni deriva ningún dato nuevo a partir de propiedades (no decide "qué significa" cargando, ese valor ya llega calculado como una propiedad booleana simple); se limita a traducir un valor ya existente (this.cargando) en una de dos plantillas alternativas, exactamente la responsabilidad que el apartado 5 reserva para render(). Si conIndicadorDeCarga calculara, en cambio, algo como "cuánto tiempo lleva cargando" a partir de una marca de tiempo, ese cálculo sí encajaría mejor en willUpdate, pero no es lo que hace en su forma actual.
  2. La reasignación this._filtroProvider.value = { ...this._filtroProvider.value, ...cambios } sigue exactamente el patrón recomendado del apartado 3: en lugar de modificar el objeto de contexto existente añadiéndole o cambiándole una propiedad en el sitio (this._filtroProvider.value.texto = cambios.texto, que mutaría el objeto sin cambiar su referencia), crea un objeto completamente nuevo mediante el operador de propagación, combinando el valor anterior con los cambios recibidos, y asigna ese objeto nuevo a .value. Esa asignación cambia la referencia del valor de contexto, exactamente lo que ContextProvider necesita para notificar correctamente a los consumidores suscritos, el mismo mecanismo de detección por referencia explicado para propiedades reactivas normales en el apartado 3.
  3. La pregunta que lo habría señalado es la primera: "¿Estoy reemplazando esta propiedad por un valor nuevo, o modificando el valor que ya tenía?", aunque aplicada aquí no a una mutación de array, sino a la relación entre el controlador y su host. Asignar directamente this.host.cercaDeVencer = ... desde dentro del controlador acopla el mecanismo interno del controlador a una propiedad reactiva concreta que el host tendría que declarar solo para dar cabida a ese detalle de implementación, exactamente el mismo tipo de acoplamiento innecesario que el apartado 6 desaconseja entre responsabilidades que deberían permanecer separadas: el controlador decide su propio resultado; el host, a través de requestUpdate(), solo se entera de que debe volver a renderizarse, sin que ambas responsabilidades se mezclen en una única propiedad compartida.

Conclusión

Esta lección ha revisado, en cinco pares, los anti-patrones más señalados a lo largo del curso frente a la alternativa recomendada que TaskFlow ha aplicado en cada caso: inmutabilidad en lugar de mutación en el sitio, composed: true para que los eventos atraviesen el Shadow DOM, cálculo derivado fuera de render(), componentes descompuestos según señales concretas, y limpieza simétrica de cualquier recurso arrancado en el ciclo de vida. Con este repaso, y con las pruebas, la accesibilidad y el rendimiento de las tres lecciones anteriores, el módulo 9 completa la cobertura de calidad de TaskFlow.

Con esto se cierra también el contenido teórico del curso al completo. Queda una única lección, la última del curso: "Proyecto: Construyendo TaskFlow", donde no se presenta ningún concepto nuevo, sino que se recorre la aplicación de principio a fin, se repasan y completan las piezas que a lo largo del curso quedaron mencionadas sin cerrar del todo, y se entrega TaskFlow como la aplicación acabada que corona todo el recorrido, desde el primer componente Lit hasta el proyecto final.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados