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

  1. El ciclo de actualización completo, paso a paso
  2. shouldUpdate: el veto, mencionado por completitud
  3. willUpdate: derivar estado antes de renderizar
  4. render(): el punto ya conocido
  5. firstUpdated: una sola vez, tras el primer render
  6. updated: después de cada render, con el DOM ya pintado
  7. updateComplete: la promesa que cierra el ciclo
  8. Tabla comparativa de los cinco puntos de enganche
  9. Aplicando willUpdate a <task-card>: urgencia derivada de la fecha
  10. Cierre: hacia la reutilización con controladores

  1. 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 componente

Todos 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.

  1. shouldUpdate: el veto, mencionado por completitud

El 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.

  1. willUpdate: derivar estado antes de renderizar

willUpdate(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.

  1. render(): el punto ya conocido

render() 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.

  1. firstUpdated: una sola vez, tras el primer render

firstUpdated(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.

  1. updated: después de cada render, con el DOM ya pintado

updated(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).

  1. updateComplete: la promesa que cierra el ciclo

this.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.

  1. 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 Una sola vez Medir el DOM, enfocar un campo, inicializar librerías externas
updated Tras aplicar cada render() al DOM En cada actualización, incluida la primera Reaccionar a cambios ya pintados en pantalla
updateComplete Se resuelve al terminar el ciclo 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).

  1. Aplicando willUpdate a <task-card>: urgencia derivada de la fecha

La 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 willUpdate con updated: son opuestos en cuanto al estado del DOM: dentro de willUpdate el 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 de updated, 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 de render() sin provocar una segunda actualización).
  • Olvidar changedProperties.has(...) dentro de willUpdate o updated: 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 el dispatchEvent del 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; cualquier querySelector ejecutado ahí puede devolver un nodo que va a desaparecer o cambiar en cuanto termine render(). Ese tipo de acceso al DOM pertenece a firstUpdated o updated.
  • Usar firstUpdated para algo que debería repetirse en cada actualización: si la lógica depende de datos que cambian con el tiempo (como titulo o estado), firstUpdated solo se ejecutaría una vez, con los valores iniciales, y quedaría desactualizada para siempre; el hook correcto en ese caso es updated.

Ejercicios

  1. Añade a <task-card> un hook updated(changedProperties) que, únicamente la primera vez que cercaDeVencer pase de false a true (y no en cada actualización posterior mientras siga siendo true), despache un evento personalizado tarea-proxima-a-vencer con bubbles: true y composed: true. Pista: changedProperties.get('cercaDeVencer') contiene el valor anterior de la propiedad, justo lo que hace falta para distinguir esta transición concreta.
  2. Explica, basándote en el apartado 3, por qué sería un error escribir la lógica del ejercicio anterior dentro de willUpdate en lugar de dentro de updated.
  3. Un compañero de equipo propone eliminar willUpdate de <task-card> y, en su lugar, llamar directamente a this._calcularSiCercaDeVencer() desde dentro de render() 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 de willUpdate.

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.

  1. willUpdate se ejecuta antes de que el DOM refleje la actualización en curso, y antes incluso de que render() se haya ejecutado. Despachar ahí un evento hacia fuera sería prematuro en el sentido conceptual del ciclo: el propio principio de updated, 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 como tarea-proxima-a-vencer, pensado para que otros componentes reaccionen a un hecho ya consumado, encaja con ese principio, no con el de willUpdate, reservado a preparar datos derivados que el propio render() todavía va a usar.

  2. Llamar a this._calcularSiCercaDeVencer() directamente dentro de render() recalcularía ese valor en cada ejecución de render(), incluidas las que no tienen absolutamente nada que ver con fechaLimite (por ejemplo, un cambio de prioridad o de expandida), 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 cuando fechaLimite cambia de verdad (mediante changedProperties.has('fechaLimite')), que es exactamente la ventaja que aporta willUpdate frente 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

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