A lo largo de este módulo has escrito varias plantillas dentro de render(), pero en varios ejemplos se ha señalado explícitamente que cambiar un campo de instancia desde la consola del navegador no actualiza la pantalla. Antes de entender por qué (y de resolverlo, en el módulo 3, con propiedades reactivas de verdad), conviene comprender cómo funciona realmente el ciclo de renderizado de Lit: cuándo decide volver a ejecutar render(), por qué ese proceso es asíncrono, y qué reglas hay que respetar al escribir ese método para no acabar con comportamientos extraños. Esta lección cierra el módulo 2 con esa base conceptual, y prepara el terreno para el módulo 3, donde <task-card> y <task-list> dejarán por fin de depender de campos de instancia sin reactividad.
Contenido
render()no se llama solo: hace falta un disparadorrequestUpdate(): el disparador manual, a alto nivel- Por qué las actualizaciones son asíncronas
- Agrupación (batching) de varios cambios en un único renderizado
render()como función pura de su estado- Por qué no se debe tocar el DOM a mano dentro de
render() - Cierre del módulo: hacia las propiedades reactivas de verdad
render() no se llama solo: hace falta un disparador
render() no se llama solo: hace falta un disparadorDurante todo este módulo, render() se ha ejecutado siempre en el mismo momento: quando el componente se inserta por primera vez en la página. Eso explica por qué, en los ejemplos anteriores, cambiar un campo de instancia desde la consola del navegador (elemento.titulo = 'Otro título') no produce ningún cambio visible: render() ya se ejecutó una vez, al insertar el componente, y nada le ha indicado a Lit que debe volver a ejecutarse.
Esto no es un defecto ni una limitación temporal del curso: es, sencillamente, cómo funciona el mecanismo interno de Lit. render() no se vuelve a llamar automáticamente porque sí; necesita un disparador explícito que le indique a Lit "algo ha cambiado, hay que actualizar". Ese disparador, en el uso normal de Lit, es un cambio en una propiedad reactiva declarada con static properties (o con el decorador @property), tal como se mencionó por encima en la lección de anatomía del módulo 1. Cuando se asigna un nuevo valor a una propiedad reactiva, Lit lo detecta internamente (mediante un mecanismo de setters que instala automáticamente sobre esas propiedades concretas) y programa una actualización.
Los campos de instancia simples usados en este módulo (this.titulo, this.estado, this.tareas...) son campos de JavaScript corrientes, sin ningún setter especial instalado por Lit encima. Asignarles un valor nuevo es una operación de JavaScript completamente normal que Lit ni siquiera llega a "ver". De ahí que, sin propiedades reactivas, no exista ningún disparador automático: el módulo 3 resolverá exactamente esta pieza que falta.
requestUpdate(): el disparador manual, a alto nivel
requestUpdate(): el disparador manual, a alto nivelAunque el disparador habitual sea el cambio de una propiedad reactiva, Lit expone también un método que cualquier componente puede llamar directamente para forzar una actualización sin depender de ese mecanismo: this.requestUpdate().
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
constructor() {
super();
this.titulo = 'Preparar la demo del sprint';
}
cambiarTitulo(nuevoTitulo) {
this.titulo = nuevoTitulo;
this.requestUpdate(); // le decimos explícitamente a Lit: "actualiza"
}
render() {
return html`<h3>${this.titulo}</h3>`;
}
}
customElements.define('task-card', TaskCard);En este ejemplo, cambiarTitulo modifica el campo this.titulo (un simple campo de instancia, igual que en el resto del módulo) y, justo después, llama a this.requestUpdate() para pedirle a Lit, de forma explícita, que vuelva a ejecutar render(). Si desde la consola del navegador se llamara a elemento.cambiarTitulo('Nuevo título'), esta vez sí se vería el cambio reflejado en pantalla, porque ha habido una llamada explícita a requestUpdate().
Es importante situar correctamente esta pieza dentro del curso: requestUpdate() es, en esencia, el mecanismo de bajo nivel que Lit usa internamente cuando detecta un cambio en una propiedad reactiva; conocerlo ahora ayuda a entender que la "magia" de las propiedades reactivas del módulo 3 no es magia en absoluto, sino exactamente esta misma llamada, disparada automáticamente por Lit en el momento adecuado. El estudio detallado de requestUpdate() —sus parámetros opcionales, en qué casos concretos conviene llamarlo manualmente incluso con propiedades reactivas ya declaradas— pertenece a los módulos 3 y 6; aquí basta con reconocer que existe y para qué sirve a grandes rasgos.
- Por qué las actualizaciones son asíncronas
Una particularidad del ciclo de renderizado de Lit, que sorprende a quien lo ve por primera vez, es que llamar a requestUpdate() (o, en el módulo 3, cambiar una propiedad reactiva) no actualiza el DOM de forma inmediata, en la misma línea de código. En su lugar, Lit programa la actualización para que se ejecute en la siguiente "microtarea" del bucle de eventos de JavaScript, un concepto propio de la plataforma web (no de Lit) que determina cuándo se ejecuta código en cola una vez termina la tarea síncrona actual.
cambiarTitulo(nuevoTitulo) {
this.titulo = nuevoTitulo;
this.requestUpdate();
// En este punto, el DOM TODAVÍA no se ha actualizado.
console.log(this.shadowRoot.querySelector('h3').textContent);
// Muestra el título ANTERIOR, no el nuevo.
}Si se necesita esperar a que la actualización se haya aplicado realmente al DOM antes de seguir ejecutando código, Lit ofrece una propiedad especial, this.updateComplete, que es una Promise que se resuelve justo cuando la actualización pendiente termina:
async cambiarTitulo(nuevoTitulo) {
this.titulo = nuevoTitulo;
this.requestUpdate();
await this.updateComplete;
// Ahora sí, el DOM ya refleja el nuevo título.
console.log(this.shadowRoot.querySelector('h3').textContent);
}No hace falta memorizar updateComplete en detalle en este punto del curso (se retomará cuando resulte relevante, más adelante); lo importante es interiorizar la idea general: una actualización de Lit no ocurre en el mismo instante en que se produce el cambio que la origina, sino un poco después, de forma asíncrona.
- Agrupación (batching) de varios cambios en un único renderizado
El hecho de que las actualizaciones sean asíncronas no es un simple detalle técnico curioso: es lo que permite a Lit una optimización importante llamada batching (agrupación). Si, dentro de la misma función síncrona, se modifican varias propiedades reactivas seguidas, Lit no ejecuta render() una vez por cada cambio; espera hasta el final de esa función y ejecuta render() una sola vez, con todos los cambios ya aplicados a la vez.
actualizarVariosCampos() {
this.titulo = 'Nuevo título';
this.estado = 'hecha';
this.prioridad = 5;
// Aunque se han modificado tres propiedades reactivas,
// Lit solo ejecutará render() una vez, no tres.
}Esta agrupación es posible precisamente gracias al retraso hasta la siguiente microtarea explicado en el apartado anterior: como la actualización real no ocurre de inmediato, Lit tiene una pequeña ventana de tiempo para "recoger" todos los cambios que se produzcan durante la ejecución síncrona actual y aplicarlos juntos en una sola pasada de render(). Si las actualizaciones fueran síncronas e inmediatas, cada una de las tres asignaciones del ejemplo anterior dispararía su propia ejecución completa de render(), desperdiciando trabajo en dos renderizados intermedios cuyo resultado nunca llega a mostrarse (porque enseguida se sobrescribe con el siguiente cambio).
Esta es una de las razones de fondo, junto con el análisis de plantillas ya cacheado explicado en la primera lección del módulo, por las que Lit puede permitirse un modelo de "cambia la propiedad y listo" sin preocuparse por el coste de actualizaciones frecuentes: el propio sistema agrupa automáticamente el trabajo.
render() como función pura de su estado
render() como función pura de su estadoUna idea que conviene fijar con claridad, porque orienta cómo se debe escribir cualquier render(): este método debe comportarse como una función pura del estado actual del componente. Es decir, dado el mismo estado interno (los mismos valores de sus propiedades y campos), render() debe devolver siempre el mismo resultado, sin producir ningún efecto secundario observable fuera de sí mismo.
En la práctica, esto significa evitar dentro de render():
- Modificar cualquier propiedad o campo del propio componente (
this.algo = ...), porque eso podría disparar una nueva actualización desde dentro de la actualización actual, generando bucles difíciles de depurar. - Realizar peticiones de red, temporizadores, o cualquier operación con efectos externos:
render()puede llegar a ejecutarse varias veces por el propio funcionamiento interno de Lit, y no es el lugar adecuado para lanzar operaciones que no deberían repetirse innecesariamente. - Depender de datos externos que cambien sin que Lit lo sepa, como leer directamente
Math.random()oDate.now()y esperar un resultado estable; si esos valores deben aparecer en la interfaz, es mejor calcularlos una vez (por ejemplo, en el constructor o en un callback del ciclo de vida, según corresponda) y guardarlos como parte del estado del componente.
Esta filosofía —render() como una traducción directa y predecible de "estado actual" a "HTML actual", sin sorpresas ni efectos colaterales— es compartida por prácticamente todas las bibliotecas modernas de interfaces, no solo por Lit, y es la que hace posible razonar sobre un componente simplemente mirando sus datos, sin tener que rastrear una cadena de efectos secundarios ocultos.
- Por qué no se debe tocar el DOM a mano dentro de
render()
render()Directamente relacionado con la idea de función pura del apartado anterior está una regla muy concreta: nunca se debe manipular el DOM manualmente dentro de render(), ni con document.querySelector, ni con this.shadowRoot.querySelector, ni asignando directamente innerHTML a algún nodo.
// Antipatrón: NO hacer esto dentro de render()
render() {
const resultado = html`<article><h3>${this.titulo}</h3></article>`;
// Mal: modificar el DOM manualmente durante el propio renderizado
const posibleH3 = this.shadowRoot?.querySelector('h3');
if (posibleH3) {
posibleH3.classList.add('ya-renderizado');
}
return resultado;
}El motivo es doble. Primero, como se explicó en la primera lección del módulo, es Lit quien decide, a partir del análisis de la plantilla, cómo y cuándo actualizar cada nodo concreto del DOM; intervenir manualmente compite con ese mecanismo y puede hacer que Lit pierda la pista de en qué estado real se encuentra un nodo, produciendo actualizaciones inconsistentes. Segundo, en el momento en que se ejecuta el cuerpo de render(), el DOM resultante de esta ejecución concreta puede que todavía no se haya aplicado a la página: render() solo devuelve una descripción de lo que debería mostrarse, no lo aplica él mismo, así que buscar nodos con querySelector dentro del propio render() puede encontrar el DOM de la versión anterior, no de la que se está generando.
Si un componente necesita ejecutar lógica que dependa del DOM ya actualizado (por ejemplo, medir el tamaño real de un elemento tras renderizarlo), el lugar correcto para hacerlo es alguno de los callbacks del ciclo de vida vistos en el módulo 1 —típicamente updated() o firstUpdated()—, que Lit garantiza que se ejecutan después de que el DOM ya refleje el resultado de render(). El funcionamiento detallado de esos callbacks es, de nuevo, contenido del módulo 6; por ahora basta con retener la regla general: render() describe, no manipula.
- Cierre del módulo: hacia las propiedades reactivas de verdad
Con esta lección se completa el recorrido del módulo 2. Has aprendido que html es una función que separa la estructura fija de una plantilla de sus valores dinámicos, permitiendo actualizaciones eficientes sin Virtual DOM; que dentro de ${} se pueden interpolar textos, atributos, propiedades del DOM y valores booleanos, además de expresiones JavaScript arbitrarias; que el renderizado condicional y el renderizado de listas no necesitan sintaxis especial, solo operadores y métodos estándar de JavaScript (?:, &&, Array.map); y, en esta última lección, que todo ese proceso de renderizado se dispara mediante requestUpdate(), ocurre de forma asíncrona y agrupada, y debe tratar render() como una función pura sin efectos secundarios sobre el DOM.
A lo largo del módulo, sin embargo, se ha repetido una limitación de forma deliberada: <task-card> y <task-list> han usado campos de instancia simples (this.titulo, this.tareas...) precisamente para poder centrar la atención en cómo se escriben las plantillas, sin la complejidad añadida del sistema de reactividad. Como se ha visto en el apartado 1 de esta lección, esos campos no disparan ninguna actualización automática porque Lit no tiene forma de saber que han cambiado: solo vigila las propiedades que se han declarado explícitamente como reactivas.
Esa es exactamente la pieza que falta, y que se resuelve en el módulo 3, "Propiedades y Estado Reactivo". Allí aprenderás a declarar titulo, estado, prioridad y tareas como propiedades reactivas de verdad mediante static properties, entendiendo cómo Lit instala automáticamente el mecanismo de detección de cambios sobre ellas, cómo distinguir entre propiedades públicas y estado interno con @state, y cómo se relacionan las propiedades con los atributos HTML. En ese momento, todo lo aprendido en este módulo 2 sobre plantillas, condicionales, listas y ciclo de renderizado se combinará con la reactividad real, y <task-card> y <task-list> empezarán, por fin, a actualizarse solas cuando cambien sus datos.
Errores Comunes y Consejos
- Esperar que el DOM esté actualizado justo después de
requestUpdate(): como se explica en el apartado 3, la actualización es asíncrona; si se necesita comprobar el DOM tras un cambio, hay que esperar athis.updateComplete(conawait) o comprobarlo en un callback posterior del ciclo de vida. - Llamar a
requestUpdate()de forma repetitiva e innecesaria: si un componente ya usa propiedades reactivas correctamente declaradas (contenido del módulo 3), casi nunca hace falta llamar arequestUpdate()manualmente; Lit ya lo hace automáticamente al detectar el cambio. Llamarlo "por si acaso" añade ruido y puede ocultar errores reales en la declaración de las propiedades. - Modificar propiedades del propio componente dentro de
render(): como se explicó en el apartado 5, esto puede generar bucles de actualización difíciles de depurar, en los que cadarender()provoca sin querer otrorender()adicional. - Manipular el DOM interno con
querySelectordentro derender(): como se explicó en el apartado 6, el lugar correcto para ese tipo de lógica son los callbacks posteriores a la actualización (updated(),firstUpdated()), no el propiorender().
Ejercicios
- Retoma el ejemplo del apartado 2 (
cambiarTituloconrequestUpdate()) y amplíalo para que, además del título, cambie también el estado y la prioridad en la misma llamada. Razona, apoyándote en el apartado 4, si esto provocará una o varias ejecuciones derender(). - Escribe, en pseudocódigo o en JavaScript real si lo prefieres, un método
esperarYComprobar()que cambiethis.titulo, llame arequestUpdate(), y después useawait this.updateCompletepara, a continuación, leer conthis.shadowRoot.querySelector('h3').textContentel título ya actualizado en el DOM real. - Identifica, en algún componente que hayas escrito en lecciones anteriores de este módulo (
task-card.jsotask-list.js), si hay alguna línea dentro derender()que infrinja la regla del apartado 6 (manipular el DOM manualmente). Si no la hay, explica por qué esos componentes, tal como están escritos, ya cumplen esa regla de forma natural.
Soluciones
cambiarVariosCampos(nuevoTitulo, nuevoEstado, nuevaPrioridad) {
this.titulo = nuevoTitulo;
this.estado = nuevoEstado;
this.prioridad = nuevaPrioridad;
this.requestUpdate();
}Aunque se llame a requestUpdate() una sola vez de forma explícita en este ejemplo (y aunque se llamara tres veces seguidas, una por cada campo), Lit agrupa cualquier actualización solicitada durante la misma ejecución síncrona y solo ejecuta render() una vez, como se explicó en el apartado 4, mostrando ya los tres valores nuevos a la vez.
async esperarYComprobar() {
this.titulo = 'Título actualizado';
this.requestUpdate();
await this.updateComplete;
const textoActual = this.shadowRoot.querySelector('h3').textContent;
console.log(textoActual); // Ahora sí, "Título actualizado"
}- En los componentes
task-card.jsytask-list.jsde este módulo,render()se limita siempre a construir y devolver una plantillahtmla partir de campos de instancia (this.titulo,this.estado,this.tareas...), sin llamar en ningún momento adocument.querySelector,this.shadowRoot.querySelector, ni a asignaciones directas deinnerHTML. Por tanto, cumplen de forma natural la regla del apartado 6: describen el resultado deseado mediantehtml, sin manipular el DOM por su cuenta, dejando esa responsabilidad exclusivamente a Lit.
Conclusión
En esta última lección del módulo 2 has entendido el mecanismo que decide cuándo y cómo Lit vuelve a ejecutar render(): un disparador (habitualmente el cambio de una propiedad reactiva, o requestUpdate() de forma manual), un proceso asíncrono que se resuelve en la siguiente microtarea y que agrupa varios cambios en una sola actualización, y una regla de diseño fundamental: render() debe comportarse como una función pura de su estado, sin modificar propiedades propias ni tocar el DOM manualmente dentro de él.
Con esto se cierra el módulo "Plantillas Reactivas y Renderizado": ya sabes escribir plantillas dinámicas con html, interpolar todo tipo de valores, renderizar condicionalmente y en forma de listas, y entiendes el ciclo interno que aplica esos cambios al DOM. Lo único que ha faltado deliberadamente durante todo el módulo es el disparador automático real: unas propiedades que, al cambiar, activen esta maquinaria por sí solas. Eso es, exactamente, el punto de partida del módulo 3, "Propiedades y Estado Reactivo", donde convertirás los campos simples de <task-card> y <task-list> en propiedades reactivas de verdad con static properties, y donde cada tarjeta de TaskFlow podrá por fin mostrar los datos de su propia tarea de forma dinámica y actualizarse sola cuando esos datos cambien.
Curso de Lit
Módulo 1: Introducción a Lit y Web Components
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
