La lección anterior ha resuelto el ciclo de vida "externo" de un elemento personalizado: cuándo entra y cuándo sale del DOM. Pero Lit añade, por encima de ese ciclo heredado del estándar, un segundo ciclo propio, mucho más frecuente durante la vida normal de un componente: el que se dispara cada vez que una propiedad reactiva cambia y Lit decide volver a ejecutar render(). El módulo 2 ya explicó que ese proceso es asíncrono y se agrupa en microtareas, y la lección 03-01 mencionó de pasada updateComplete; esta lección completa el mapa, ordenando con precisión las fases de una actualización y los puntos exactos en los que un componente puede engancharse a cada una de ellas.
Contenido
- El ciclo de actualización completo, paso a paso
shouldUpdate: el veto, mencionado por completitudwillUpdate: derivar estado antes de renderizarrender(): el punto ya conocidofirstUpdated: una sola vez, tras el primer renderupdated: después de cada render, con el DOM ya pintadoupdateComplete: la promesa que cierra el ciclo- Tabla comparativa de los cinco puntos de enganche
- Aplicando
willUpdatea<task-card>: urgencia derivada de la fecha - Cierre: hacia la reutilización con controladores
- El ciclo de actualización completo, paso a paso
Cuando una propiedad reactiva cambia (o se llama a this.requestUpdate() manualmente, como se vio en el módulo 2), Lit no ejecuta simplemente render() y ya está: recorre una secuencia fija de pasos, siempre en el mismo orden, cada uno de los cuales puede sobreescribirse como método de la clase:
cambio de propiedad
│
▼
shouldUpdate(changedProperties) ← puede devolver false y detener aquí todo el ciclo
│
▼
willUpdate(changedProperties) ← antes de renderizar; el DOM todavía no refleja el cambio
│
▼
render() ← devuelve la plantilla; ya conocido desde el módulo 2
│
▼
(Lit aplica el resultado de render() al DOM real)
│
▼
firstUpdated(changedProperties) ← solo la primera vez, con el DOM ya actualizado
│
▼
updated(changedProperties) ← todas las veces, con el DOM ya actualizado
│
▼
this.updateComplete se resuelve ← promesa observable desde fuera del componenteTodos estos métodos, salvo render(), reciben un mismo argumento: changedProperties, un objeto de tipo Map cuyas claves son los nombres de las propiedades que han cambiado en esta actualización concreta, y cuyos valores son el valor anterior de cada una (no el nuevo: el nuevo ya está disponible directamente en this.nombreDeLaPropiedad). Este mapa es la herramienta que permite, dentro de cualquiera de estos hooks, distinguir qué ha cambiado exactamente y reaccionar solo a lo que interesa, en lugar de recalcularlo todo en cada actualización sin necesidad.
shouldUpdate: el veto, mencionado por completitud
shouldUpdate: el veto, mencionado por completitudEl primer punto de enganche del ciclo, shouldUpdate(changedProperties), decide si la actualización debe continuar en absoluto. Si se sobreescribe y devuelve false, Lit detiene el ciclo ahí mismo: ni willUpdate, ni render(), ni ningún otro hook posterior se ejecutan para esa actualización concreta.
shouldUpdate(changedProperties) {
// Ejemplo ilustrativo: ignorar cualquier actualización mientras
// la tarjeta esté en un estado de "solo lectura" temporal.
if (this._bloqueadaTemporalmente) {
return false;
}
return true; // el valor por defecto, heredado de LitElement, siempre es true
}Es una herramienta de optimización de rendimiento para casos concretos (evitar renderizados costosos cuando se sabe, de antemano, que el resultado visual no cambiaría), y no es el foco de esta lección: <task-card> y el resto de componentes de TaskFlow no lo necesitan por ahora. Se menciona aquí únicamente para completar el mapa del apartado 1 y para que quede claro en qué punto exacto del ciclo se sitúa, antes de pasar a los hooks que sí se van a usar activamente.
willUpdate: derivar estado antes de renderizar
willUpdate: derivar estado antes de renderizarwillUpdate(changedProperties) se ejecuta justo antes de render(), en cada actualización (incluida la primera). Es el lugar pensado específicamente para derivar o recalcular estado interno a partir de los cambios de propiedades que se acaban de producir, de forma que ese cálculo esté ya disponible cuando render() se ejecute a continuación, sin tener que repetir la misma lógica dentro del propio render().
willUpdate(changedProperties) {
if (changedProperties.has('prioridad')) {
this._etiquetaPrioridad = this.prioridad >= 4 ? 'Alta' : 'Normal';
}
}Un detalle importante: dentro de willUpdate es seguro asignar nuevas propiedades reactivas o campos de instancia normales, porque todavía no se ha llamado a render() en esta pasada; cualquier asignación hecha aquí queda incorporada a la misma actualización en curso, sin disparar un segundo ciclo completo adicional. Esto es justo lo contrario de lo que se advirtió en el módulo 2 sobre modificar propiedades dentro del propio render() (que sí puede generar bucles de actualización): willUpdate existe precisamente para dar un lugar seguro a este tipo de cálculo derivado, antes de que la plantilla se construya.
changedProperties.has('nombre') es el patrón habitual para no recalcular más de lo necesario: comprobar primero si la propiedad relevante ha cambiado en esta actualización concreta, y solo entonces ejecutar el cálculo derivado correspondiente. Sin esa comprobación, willUpdate recalcularía lo mismo en cada actualización, incluso en las que no tienen nada que ver con ese dato en particular; para cálculos baratos no supone ningún problema real, pero es una buena costumbre en cuanto el cálculo empieza a tener algo de coste.
render(): el punto ya conocido
render(): el punto ya conocidorender() ocupa, en este ciclo más completo, exactamente el lugar que ya se conoce desde el módulo 2: construye y devuelve la plantilla html que describe el estado actual del componente. No hay nada nuevo que añadir aquí salvo su posición relativa: se ejecuta siempre después de willUpdate (que ya ha tenido ocasión de preparar cualquier dato derivado) y siempre antes de que el DOM real quede actualizado.
firstUpdated: una sola vez, tras el primer render
firstUpdated: una sola vez, tras el primer renderfirstUpdated(changedProperties) se ejecuta una única vez en toda la vida del componente: justo después de que la primera actualización se haya aplicado ya al DOM real. A partir de este punto del ciclo, a diferencia de willUpdate, sí se puede consultar con garantías el DOM del propio Shadow Root, porque ya refleja el resultado de render().
firstUpdated(changedProperties) {
// El shadow root ya contiene el <article> devuelto por el primer render().
const articulo = this.shadowRoot.querySelector('article');
console.log('Altura inicial de la tarjeta:', articulo.offsetHeight);
}Es el lugar recomendado por la propia documentación de Lit para tareas que solo tiene sentido hacer una vez, y que necesitan el DOM ya construido: medir el tamaño real de un elemento, poner el foco inicial en un campo, o inicializar una librería externa de terceros que necesite un nodo del DOM real al que enlazarse. Nada de esto encaja en willUpdate (donde el DOM todavía no se ha actualizado) ni tendría sentido repetirlo en cada actualización posterior, que es justo lo que distingue a firstUpdated de updated.
updated: después de cada render, con el DOM ya pintado
updated: después de cada render, con el DOM ya pintadoupdated(changedProperties) es el hermano de firstUpdated que sí se ejecuta en todas las actualizaciones, incluida la primera (de hecho, en la primera actualización, Lit llama primero a firstUpdated y justo después a updated; en las siguientes, solo a updated). Al igual que firstUpdated, se ejecuta con el DOM ya actualizado, así que es seguro consultar el Shadow Root con la certeza de que refleja el resultado más reciente de render().
updated(changedProperties) {
if (changedProperties.has('estado') && this.estado === 'hecha') {
console.log(`La tarea "${this.titulo}" se ha marcado como hecha`);
}
}El uso típico de updated es reaccionar a un cambio que ya se ha pintado en pantalla: reproducir una animación puntual, sincronizar algo con una librería externa que necesita saber que el contenido ha cambiado, o (como en el ejemplo del apartado 9) despachar un evento hacia fuera únicamente cuando cierta condición pasa a cumplirse. La comprobación con changedProperties.has(...) vuelve a ser fundamental aquí: sin ella, la lógica dentro de updated se ejecutaría en cada actualización del componente, sin importar qué haya cambiado realmente, lo cual suele producir efectos repetidos de forma innecesaria (o, en el peor de los casos, errónea).
updateComplete: la promesa que cierra el ciclo
updateComplete: la promesa que cierra el ciclothis.updateComplete, ya introducida de pasada en el módulo 2, es una propiedad especial de tipo Promise que se resuelve exactamente cuando el ciclo completo de la actualización en curso termina, es decir, después de que updated se haya ejecutado. A diferencia de los cuatro hooks anteriores, que son métodos que se sobreescriben dentro de la clase del componente, updateComplete está pensada para consultarse desde fuera, por cualquier código que necesite saber cuándo un componente ha terminado de aplicar sus cambios más recientes:
const tarjeta = document.querySelector('task-card');
tarjeta.estado = 'hecha';
await tarjeta.updateComplete;
// En este punto, render(), firstUpdated (si procedía) y updated
// ya se han ejecutado, y el DOM de la tarjeta refleja el nuevo estado.Dentro de la propia clase del componente, casi nunca hace falta usar this.updateComplete: los hooks internos (willUpdate, updated, firstUpdated) ya cubren cualquier necesidad de reaccionar al ciclo desde dentro. updateComplete resulta más útil en tests automatizados (esperar a que un cambio se haya aplicado antes de hacer una aserción, algo que se retomará en el módulo 9) o en código externo al propio árbol de componentes de Lit que necesite sincronizarse con el ritmo de actualización de un componente concreto.
- Tabla comparativa de los cinco puntos de enganche
| Hook | ¿Cuándo se ejecuta? | ¿DOM ya actualizado? | ¿Cuántas veces? | Uso principal |
|---|---|---|---|---|
shouldUpdate |
Antes de willUpdate |
No | En cada actualización | Vetar una actualización completa (return false) |
willUpdate |
Justo antes de render() |
No | En cada actualización, incluida la primera | Derivar/recalcular estado interno a partir de propiedades cambiadas |
render() |
Construye la plantilla | No aplica | En cada actualización | Devolver el html que describe el estado actual |
firstUpdated |
Tras aplicar el primer render() al DOM |
Sí | Una sola vez | Medir el DOM, enfocar un campo, inicializar librerías externas |
updated |
Tras aplicar cada render() al DOM |
Sí | En cada actualización, incluida la primera | Reaccionar a cambios ya pintados en pantalla |
updateComplete |
Se resuelve al terminar el ciclo | Sí | Una promesa por cada ciclo, consultable desde fuera | Esperar, desde fuera del componente, a que termine una actualización |
Una forma sencilla de recordar cuándo usar willUpdate frente a updated, que resume bien el criterio de toda esta lección: si la lógica necesita el valor de una propiedad para calcular otro valor derivado que render() va a usar, va en willUpdate; si la lógica necesita el DOM ya renderizado, o produce un efecto hacia fuera del propio proceso de renderizado, va en updated (o en firstUpdated, si solo debe ocurrir una vez).
- Aplicando
willUpdate a <task-card>: urgencia derivada de la fecha
willUpdate a <task-card>: urgencia derivada de la fechaLa lección anterior ha añadido a <task-card> un aviso de "cerca de vencer", recalculado periódicamente con un temporizador, porque el paso del tiempo por sí solo puede activarlo sin que cambie ninguna propiedad. Existe, sin embargo, un problema relacionado pero distinto: cuando se asigna una nueva fechaLimite a una tarjeta (por ejemplo, si en el futuro TaskFlow permite editar la fecha límite de una tarea ya creada), interesa recalcular de inmediato si esa nueva fecha cae dentro de un margen "urgente" a corto plazo, sin esperar al siguiente tick del temporizador y sin duplicar el cálculo directamente dentro de render(). Este es exactamente el caso de uso que willUpdate resuelve mejor que cualquier otro hook: reaccionar a un cambio de propiedad concreto, no al paso del tiempo.
class TaskCard extends LitElement {
static properties = {
// ...resto de propiedades sin cambios...
fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
cercaDeVencer: { state: true },
};
willUpdate(changedProperties) {
if (changedProperties.has('fechaLimite')) {
// Recalcula de inmediato, sin esperar al siguiente tick del
// temporizador de la lección anterior, en cuanto la fecha límite
// cambia por asignación directa de la propiedad.
this.cercaDeVencer = this._calcularSiCercaDeVencer();
}
}
// _calcularSiCercaDeVencer(), connectedCallback() y disconnectedCallback()
// siguen exactamente igual que en la lección anterior.
render() {
return html`
<article @click="${this.alternarExpandida}">
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
${this.renderSelectorEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
${this.cercaDeVencer ? html`<p class="aviso">⏰ Está a punto de vencer</p>` : ''}
${this.expandida
? html`<div class="detalle"><p>Estado interno: la tarjeta está expandida.</p></div>`
: ''}
</article>
`;
}
}Nótese cómo willUpdate y el temporizador de connectedCallback conviven sin pisarse: ambos escriben en la misma propiedad de estado, cercaDeVencer, pero reaccionan a disparadores distintos y complementarios. El temporizador cubre el caso en el que nada cambia en la tarjeta, pero el reloj avanza (una tarea puede volverse urgente sin que nadie la toque); willUpdate cubre el caso en el que la fecha límite cambia explícitamente, y conviene reflejarlo de inmediato en la misma actualización que ya se va a producir por ese cambio de propiedad, en lugar de esperar hasta el siguiente tick del intervalo (que podría tardar hasta un minuto en dispararse, según la configuración de la lección anterior). Ninguno de los dos mecanismos sustituye al otro: se complementan, cada uno cubriendo un disparador distinto para el mismo dato derivado.
Merece la pena remarcar, para cerrar este apartado, por qué esta lógica no vive directamente dentro de render(): si render() llamara a this._calcularSiCercaDeVencer() en cada ejecución, el resultado sería funcionalmente equivalente, pero se estaría recalculando ese valor en cada renderizado (incluidos los que no tienen nada que ver con fechaLimite, como un simple cambio de prioridad), y además se perdería la posibilidad de comparar explícitamente con changedProperties.has('fechaLimite') para saber si merece la pena recalcular algo en absoluto. willUpdate permite concentrar esa decisión en un único lugar, ejecutada solo cuando corresponde, dejando a render() con su responsabilidad original: leer this.cercaDeVencer ya calculado y traducirlo a HTML, sin decidir nada por su cuenta.
Errores Comunes y Consejos
- Confundir
willUpdateconupdated: son opuestos en cuanto al estado del DOM: dentro dewillUpdateel DOM todavía no refleja los cambios de esta actualización (por eso es seguro derivar estado ahí, pero inútil intentar leer el DOM ya actualizado); dentro deupdated, el DOM ya está al día (por eso es el lugar correcto para leerlo o para producir efectos hacia fuera, pero ya es tarde para que cualquier cambio de propiedad hecho ahí se incorpore a esta misma pasada derender()sin provocar una segunda actualización). - Olvidar
changedProperties.has(...)dentro dewillUpdateoupdated: sin esa comprobación, la lógica se ejecuta en todas las actualizaciones, sin distinguir cuáles son realmente relevantes; para cálculos con algún coste, o para efectos que no deberían repetirse sin motivo (como eldispatchEventdel apartado 9), esta omisión produce trabajo innecesario o comportamientos duplicados. - Intentar manipular el DOM del Shadow Root dentro de
willUpdate: en ese punto del ciclo, como se ha explicado, el DOM todavía corresponde a la actualización anterior; cualquierquerySelectorejecutado ahí puede devolver un nodo que va a desaparecer o cambiar en cuanto terminerender(). Ese tipo de acceso al DOM pertenece afirstUpdatedoupdated. - Usar
firstUpdatedpara algo que debería repetirse en cada actualización: si la lógica depende de datos que cambian con el tiempo (comotitulooestado),firstUpdatedsolo se ejecutaría una vez, con los valores iniciales, y quedaría desactualizada para siempre; el hook correcto en ese caso esupdated.
Ejercicios
- Añade a
<task-card>un hookupdated(changedProperties)que, únicamente la primera vez quecercaDeVencerpase defalseatrue(y no en cada actualización posterior mientras siga siendotrue), despache un evento personalizadotarea-proxima-a-vencerconbubbles: trueycomposed: true. Pista:changedProperties.get('cercaDeVencer')contiene el valor anterior de la propiedad, justo lo que hace falta para distinguir esta transición concreta. - Explica, basándote en el apartado 3, por qué sería un error escribir la lógica del ejercicio anterior dentro de
willUpdateen lugar de dentro deupdated. - Un compañero de equipo propone eliminar
willUpdatede<task-card>y, en su lugar, llamar directamente athis._calcularSiCercaDeVencer()desde dentro derender()cada vez que se necesite el valor. Explica, apoyándote en el cierre del apartado 9, al menos una desventaja concreta de ese enfoque frente al uso dewillUpdate.
Soluciones
updated(changedProperties) {
if (changedProperties.has('cercaDeVencer')) {
const eraCercaDeVencer = changedProperties.get('cercaDeVencer');
if (!eraCercaDeVencer && this.cercaDeVencer) {
this.dispatchEvent(
new CustomEvent('tarea-proxima-a-vencer', {
detail: { titulo: this.titulo },
bubbles: true,
composed: true,
})
);
}
}
}changedProperties.get('cercaDeVencer') devuelve el valor que tenía la propiedad antes de esta actualización (false, si es la transición que interesa detectar); comparando ese valor anterior con this.cercaDeVencer (ya actualizado a true) se aísla exactamente el instante en que la tarjeta entra en la ventana de urgencia, sin repetir el aviso en actualizaciones posteriores donde cercaDeVencer siga siendo true sin haber cambiado.
-
willUpdatese ejecuta antes de que el DOM refleje la actualización en curso, y antes incluso de querender()se haya ejecutado. Despachar ahí un evento hacia fuera sería prematuro en el sentido conceptual del ciclo: el propio principio deupdated, señalado en el apartado 6, es precisamente servir de lugar para efectos que deben ocurrir después de que el cambio ya se haya aplicado visualmente; un evento comotarea-proxima-a-vencer, pensado para que otros componentes reaccionen a un hecho ya consumado, encaja con ese principio, no con el dewillUpdate, reservado a preparar datos derivados que el propiorender()todavía va a usar. -
Llamar a
this._calcularSiCercaDeVencer()directamente dentro derender()recalcularía ese valor en cada ejecución derender(), incluidas las que no tienen absolutamente nada que ver confechaLimite(por ejemplo, un cambio deprioridado deexpandida), desperdiciando trabajo en un cálculo que, la mayoría de las veces, daría exactamente el mismo resultado que ya tenía. Además, se perdería la posibilidad de reaccionar de forma selectiva solo cuandofechaLimitecambia de verdad (mediantechangedProperties.has('fechaLimite')), que es exactamente la ventaja que aportawillUpdatefrente a repetir el cálculo sin condición dentro de la propia plantilla.
Conclusión
Esta lección ha completado el mapa del ciclo de actualización propio de Lit: shouldUpdate como veto opcional, willUpdate para derivar estado antes de renderizar, render() en el punto ya conocido, firstUpdated para lo que solo debe ocurrir una vez con el DOM ya construido, updated para reaccionar a cada actualización ya pintada, y updateComplete como la promesa que permite a código externo esperar a que ese ciclo termine. <task-card> usa ya willUpdate para recalcular su urgencia por fecha en cuanto fechaLimite cambia, sin duplicar esa lógica dentro de render(), complementando al temporizador de la lección anterior en lugar de sustituirlo.
Toda esta lógica de temporizador, sin embargo, vive todavía directamente dentro de la clase TaskCard, mezclada con el resto de su comportamiento. Si TaskFlow necesitara, más adelante, el mismo tipo de aviso por proximidad de fecha en otro componente distinto (por ejemplo, un futuro resumen de tareas urgentes en el propio <task-board>), tendría que duplicar connectedCallback, disconnectedCallback y _calcularSiCercaDeVencer() por completo. La siguiente lección presenta la herramienta que Lit ofrece precisamente para evitar esa duplicación: los controladores reactivos.
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
