La lliçó anterior ha resolt el cicle de vida "extern" d'un element personalitzat: quan entra i quan surt del DOM. Però Lit afegeix, per damunt d'aquest cicle heretat de l'estàndard, un segon cicle propi, molt més freqüent durant la vida normal d'un component: el que es dispara cada vegada que una propietat reactiva canvia i Lit decideix tornar a executar render(). El mòdul 2 ja va explicar que aquest procés és asíncron i s'agrupa en microtasques, i la lliçó 03-01 va esmentar de passada updateComplete; aquesta lliçó completa el mapa, ordenant amb precisió les fases d'una actualització i els punts exactes en els quals un component es pot enganxar a cada una d'elles.

Contingut

  1. El cicle d'actualització complet, pas a pas
  2. shouldUpdate: el veto, esmentat per completesa
  3. willUpdate: derivar estat abans de renderitzar
  4. render(): el punt ja conegut
  5. firstUpdated: una sola vegada, després del primer render
  6. updated: després de cada render, amb el DOM ja pintat
  7. updateComplete: la promesa que tanca el cicle
  8. Taula comparativa dels cinc punts d'enganxament
  9. Aplicant willUpdate a <task-card>: urgència derivada de la data
  10. Tancament: cap a la reutilització amb controladors

  1. El cicle d'actualització complet, pas a pas

Quan una propietat reactiva canvia (o es crida this.requestUpdate() manualment, com es va veure al mòdul 2), Lit no executa simplement render() i ja està: recorre una seqüència fixa de passos, sempre en el mateix ordre, cada un dels quals es pot sobreescriure com a mètode de la classe:

canvi de propietat
      │
      ▼
shouldUpdate(changedProperties)   ← pot retornar false i aturar aquí tot el cicle
      │
      ▼
willUpdate(changedProperties)     ← abans de renderitzar; el DOM encara no reflecteix el canvi
      │
      ▼
render()                          ← retorna la plantilla; ja conegut des del mòdul 2
      │
      ▼
   (Lit aplica el resultat de render() al DOM real)
      │
      ▼
firstUpdated(changedProperties)   ← només la primera vegada, amb el DOM ja actualitzat
      │
      ▼
updated(changedProperties)        ← totes les vegades, amb el DOM ja actualitzat
      │
      ▼
this.updateComplete es resol      ← promesa observable des de fora del component

Tots aquests mètodes, excepte render(), reben un mateix argument: changedProperties, un objecte de tipus Map les claus del qual són els noms de les propietats que han canviat en aquesta actualització concreta, i els valors del qual són el valor anterior de cada una (no el nou: el nou ja està disponible directament a this.nombreDeLaPropiedad). Aquest mapa és l'eina que permet, dins de qualsevol d'aquests hooks, distingir què ha canviat exactament i reaccionar només a allò que interessa, en lloc de recalcular-ho tot en cada actualització sense necessitat.

  1. shouldUpdate: el veto, esmentat per completesa

El primer punt d'enganxament del cicle, shouldUpdate(changedProperties), decideix si l'actualització ha de continuar en absolut. Si es sobreescriu i retorna false, Lit atura el cicle allà mateix: ni willUpdate, ni render(), ni cap altre hook posterior s'executen per a aquesta actualització concreta.

shouldUpdate(changedProperties) {
  // Exemple il·lustratiu: ignorar qualsevol actualització mentre
  // la targeta estigui en un estat de "només lectura" temporal.
  if (this._bloqueadaTemporalmente) {
    return false;
  }
  return true; // el valor per defecte, heretat de LitElement, sempre és true
}

És una eina d'optimització de rendiment per a casos concrets (evitar renderitzats costosos quan se sap, d'antuvi, que el resultat visual no canviaria), i no és el focus d'aquesta lliçó: <task-card> i la resta de components de TaskFlow no la necessiten per ara. Es menciona aquí únicament per completar el mapa de l'apartat 1 i perquè quedi clar en quin punt exacte del cicle se situa, abans de passar als hooks que sí que es faran servir activament.

  1. willUpdate: derivar estat abans de renderitzar

willUpdate(changedProperties) s'executa just abans de render(), en cada actualització (inclosa la primera). És el lloc pensat específicament per derivar o recalcular estat intern a partir dels canvis de propietats que s'acaben de produir, de manera que aquest càlcul ja estigui disponible quan render() s'executi a continuació, sense haver de repetir la mateixa lògica dins del propi render().

willUpdate(changedProperties) {
  if (changedProperties.has('prioridad')) {
    this._etiquetaPrioridad = this.prioridad >= 4 ? 'Alta' : 'Normal';
  }
}

Un detall important: dins de willUpdate és segur assignar noves propietats reactives o camps d'instància normals, perquè encara no s'ha cridat render() en aquesta passada; qualsevol assignació feta aquí queda incorporada a la mateixa actualització en curs, sense disparar un segon cicle complet addicional. Això és just el contrari del que s'advertia al mòdul 2 sobre modificar propietats dins del propi render() (que sí que pot generar bucles d'actualització): willUpdate existeix precisament per donar un lloc segur a aquest tipus de càlcul derivat, abans que la plantilla es construeixi.

changedProperties.has('nombre') és el patró habitual per no recalcular més del necessari: comprovar primer si la propietat rellevant ha canviat en aquesta actualització concreta, i només aleshores executar el càlcul derivat corresponent. Sense aquesta comprovació, willUpdate recalcularia el mateix a cada actualització, fins i tot en aquelles que no tenen res a veure amb aquesta dada en particular; per a càlculs barats no suposa cap problema real, però és un bon costum en quant el càlcul comença a tenir algun cost.

  1. render(): el punt ja conegut

render() ocupa, en aquest cicle més complet, exactament el lloc que ja es coneix des del mòdul 2: construeix i retorna la plantilla html que descriu l'estat actual del component. No hi ha res de nou a afegir aquí excepte la seva posició relativa: s'executa sempre després de willUpdate (que ja ha tingut ocasió de preparar qualsevol dada derivada) i sempre abans que el DOM real quedi actualitzat.

  1. firstUpdated: una sola vegada, després del primer render

firstUpdated(changedProperties) s'executa una única vegada en tota la vida del component: just després que la primera actualització s'hagi aplicat ja al DOM real. A partir d'aquest punt del cicle, a diferència de willUpdate, sí que es pot consultar amb garanties el DOM del propi Shadow Root, perquè ja reflecteix el resultat de render().

firstUpdated(changedProperties) {
  // El shadow root ja conté l'<article> retornat pel primer render().
  const articulo = this.shadowRoot.querySelector('article');
  console.log('Altura inicial de la tarjeta:', articulo.offsetHeight);
}

És el lloc recomanat per la pròpia documentació de Lit per a tasques que només té sentit fer una vegada, i que necessiten el DOM ja construït: mesurar la mida real d'un element, posar el focus inicial en un camp, o inicialitzar una llibreria externa de tercers que necessiti un node del DOM real al qual enllaçar-se. Res d'això encaixa a willUpdate (on el DOM encara no s'ha actualitzat) ni tindria sentit repetir-ho en cada actualització posterior, que és just el que distingeix firstUpdated d'updated.

  1. updated: després de cada render, amb el DOM ja pintat

updated(changedProperties) és el germà de firstUpdated que sí que s'executa en totes les actualitzacions, inclosa la primera (de fet, en la primera actualització, Lit crida primer firstUpdated i just després updated; en les següents, només updated). Igual que firstUpdated, s'executa amb el DOM ja actualitzat, així que és segur consultar el Shadow Root amb la certesa que reflecteix el resultat més recent de render().

updated(changedProperties) {
  if (changedProperties.has('estado') && this.estado === 'hecha') {
    console.log(`La tarea "${this.titulo}" se ha marcado como hecha`);
  }
}

L'ús típic d'updated és reaccionar a un canvi que ja s'ha pintat a la pantalla: reproduir una animació puntual, sincronitzar alguna cosa amb una llibreria externa que necessita saber que el contingut ha canviat, o (com a l'exemple de l'apartat 9) despatxar un esdeveniment cap a fora únicament quan certa condició passa a complir-se. La comprovació amb changedProperties.has(...) torna a ser fonamental aquí: sense ella, la lògica dins d'updated s'executaria a cada actualització del component, sense importar què hagi canviat realment, la qual cosa sol produir efectes repetits de manera innecessària (o, en el pitjor dels casos, errònia).

  1. updateComplete: la promesa que tanca el cicle

this.updateComplete, ja introduïda de passada al mòdul 2, és una propietat especial de tipus Promise que es resol exactament quan el cicle complet de l'actualització en curs acaba, és a dir, després que updated s'hagi executat. A diferència dels quatre hooks anteriors, que són mètodes que se sobreescriuen dins de la classe del component, updateComplete està pensada per consultar-se des de fora, per qualsevol codi que necessiti saber quan un component ha acabat d'aplicar els seus canvis més recents:

const tarjeta = document.querySelector('task-card');
tarjeta.estado = 'hecha';

await tarjeta.updateComplete;
// En aquest punt, render(), firstUpdated (si calia) i updated
// ja s'han executat, i el DOM de la targeta reflecteix el nou estat.

Dins de la pròpia classe del component, gairebé mai cal usar this.updateComplete: els hooks interns (willUpdate, updated, firstUpdated) ja cobreixen qualsevol necessitat de reaccionar al cicle des de dins. updateComplete resulta més útil en tests automatitzats (esperar que un canvi s'hagi aplicat abans de fer una asserció, cosa que es retindrà al mòdul 9) o en codi extern al propi arbre de components de Lit que necessiti sincronitzar-se amb el ritme d'actualització d'un component concret.

  1. Taula comparativa dels cinc punts d'enganxament

Hook Quan s'executa? El DOM ja està actualitzat? Quantes vegades? Ús principal
shouldUpdate Abans de willUpdate No A cada actualització Vetar una actualització completa (return false)
willUpdate Just abans de render() No A cada actualització, inclosa la primera Derivar/recalcular estat intern a partir de propietats canviades
render() Construeix la plantilla No aplica A cada actualització Retornar l'html que descriu l'estat actual
firstUpdated Després d'aplicar el primer render() al DOM Una sola vegada Mesurar el DOM, enfocar un camp, inicialitzar llibreries externes
updated Després d'aplicar cada render() al DOM A cada actualització, inclosa la primera Reaccionar a canvis ja pintats a la pantalla
updateComplete Es resol en acabar el cicle Una promesa per cada cicle, consultable des de fora Esperar, des de fora del component, que acabi una actualització

Una forma senzilla de recordar quan usar willUpdate en front d'updated, que resumeix bé el criteri de tota aquesta lliçó: si la lògica necessita el valor d'una propietat per calcular un altre valor derivat que render() farà servir, va a willUpdate; si la lògica necessita el DOM ja renderitzat, o produeix un efecte cap fora del propi procés de renderitzat, va a updated (o a firstUpdated, si només ha de passar una vegada).

  1. Aplicant willUpdate a <task-card>: urgència derivada de la data

La lliçó anterior ha afegit a <task-card> un avís de "a prop de vèncer", recalculat periòdicament amb un temporitzador, perquè el pas del temps per si sol pot activar-lo sense que canviï cap propietat. Existeix, però, un problema relacionat però diferent: quan s'assigna una nova fechaLimite a una targeta (per exemple, si en el futur TaskFlow permet editar la data límit d'una tasca ja creada), interessa recalcular immediatament si aquesta nova data cau dins d'un marge "urgent" a curt termini, sense esperar al següent tick del temporitzador i sense duplicar el càlcul directament dins de render(). Aquest és exactament el cas d'ús que willUpdate resol millor que cap altre hook: reaccionar a un canvi de propietat concret, no al pas del temps.

class TaskCard extends LitElement {
  static properties = {
    // ...resta de propietats sense canvis...
    fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
    cercaDeVencer: { state: true },
  };

  willUpdate(changedProperties) {
    if (changedProperties.has('fechaLimite')) {
      // Recalcula immediatament, sense esperar al següent tick del
      // temporitzador de la lliçó anterior, en quant la data límit
      // canvia per assignació directa de la propietat.
      this.cercaDeVencer = this._calcularSiCercaDeVencer();
    }
  }

  // _calcularSiCercaDeVencer(), connectedCallback() i disconnectedCallback()
  // continuen exactament igual que a la lliçó 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>
    `;
  }
}

Val la pena notar com willUpdate i el temporitzador de connectedCallback convivencen sense trepitjar-se: tots dos escriuen a la mateixa propietat d'estat, cercaDeVencer, però reaccionen a disparadors diferents i complementaris. El temporitzador cobreix el cas en el qual res canvia a la targeta, però el rellotge avança (una tasca pot tornar-se urgent sense que ningú la toqui); willUpdate cobreix el cas en el qual la data límit canvia explícitament, i convé reflectir-ho immediatament en la mateixa actualització que ja es va a produir per aquest canvi de propietat, en lloc d'esperar fins al següent tick de l'interval (que podria trigar fins a un minut a disparar-se, segons la configuració de la lliçó anterior). Cap dels dos mecanismes substitueix l'altre: es complementen, cada un cobrint un disparador diferent per a la mateixa dada derivada.

Val la pena remarcar, per tancar aquest apartat, per què aquesta lògica no viu directament dins de render(): si render() cridés this._calcularSiCercaDeVencer() a cada execució, el resultat seria funcionalment equivalent, però s'estaria recalculant aquest valor a cada renderitzat (inclosos els que no tenen res a veure amb fechaLimite, com un simple canvi de prioridad), i a més es perdria la possibilitat de comparar explícitament amb changedProperties.has('fechaLimite') per saber si val la pena recalcular alguna cosa en absolut. willUpdate permet concentrar aquesta decisió en un únic lloc, executada només quan correspon, deixant render() amb la seva responsabilitat original: llegir this.cercaDeVencer ja calculat i traduir-lo a HTML, sense decidir res pel seu compte.

Errors Comuns i Consells

  • Confondre willUpdate amb updated: són oposats quant a l'estat del DOM: dins de willUpdate el DOM encara no reflecteix els canvis d'aquesta actualització (per això és segur derivar estat allà, però inútil intentar llegir el DOM ja actualitzat); dins d'updated, el DOM ja està al dia (per això és el lloc correcte per llegir-lo o per produir efectes cap a fora, però ja és tard perquè qualsevol canvi de propietat fet allà s'incorpori a aquesta mateixa passada de render() sense provocar una segona actualització).
  • Oblidar changedProperties.has(...) dins de willUpdate o updated: sense aquesta comprovació, la lògica s'executa a totes les actualitzacions, sense distingir quines són realment rellevants; per a càlculs amb algun cost, o per a efectes que no s'haurien de repetir sense motiu (com el dispatchEvent de l'apartat 9), aquesta omissió produeix feina innecessària o comportaments duplicats.
  • Intentar manipular el DOM del Shadow Root dins de willUpdate: en aquest punt del cicle, com s'ha explicat, el DOM encara correspon a l'actualització anterior; qualsevol querySelector executat allà pot retornar un node que desapareixerà o canviarà en acabar render(). Aquest tipus d'accés al DOM pertany a firstUpdated o updated.
  • Usar firstUpdated per a alguna cosa que hauria de repetir-se a cada actualització: si la lògica depèn de dades que canvien amb el temps (com titulo o estado), firstUpdated només s'executaria una vegada, amb els valors inicials, i quedaria desactualitzada per sempre; el hook correcte en aquest cas és updated.

Exercicis

  1. Afegeix a <task-card> un hook updated(changedProperties) que, únicament la primera vegada que cercaDeVencer passi de false a true (i no a cada actualització posterior mentre continuï sent true), despatxi un esdeveniment personalitzat tarea-proxima-a-vencer amb bubbles: true i composed: true. Pista: changedProperties.get('cercaDeVencer') conté el valor anterior de la propietat, just el que cal per distingir aquesta transició concreta.
  2. Explica, basant-te en l'apartat 3, per què seria un error escriure la lògica de l'exercici anterior dins de willUpdate en lloc de dins d'updated.
  3. Un company d'equip proposa eliminar willUpdate de <task-card> i, en el seu lloc, cridar directament this._calcularSiCercaDeVencer() des de dins de render() cada vegada que es necessiti el valor. Explica, recolzant-te en el tancament de l'apartat 9, almenys un desavantatge concret d'aquest enfocament en front de l'ús de willUpdate.

Solucions

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') retorna el valor que tenia la propietat abans d'aquesta actualització (false, si és la transició que interessa detectar); comparant aquest valor anterior amb this.cercaDeVencer (ja actualitzat a true) s'aïlla exactament l'instant en què la targeta entra a la finestra d'urgència, sense repetir l'avís en actualitzacions posteriors on cercaDeVencer continuï sent true sense haver canviat.

  1. willUpdate s'executa abans que el DOM reflecteixi l'actualització en curs, i abans fins i tot que render() s'hagi executat. Despatxar allà un esdeveniment cap a fora seria prematur en el sentit conceptual del cicle: el propi principi d'updated, assenyalat a l'apartat 6, és precisament servir de lloc per a efectes que han de passar després que el canvi ja s'hagi aplicat visualment; un esdeveniment com tarea-proxima-a-vencer, pensat perquè altres components reaccionin a un fet ja consumat, encaixa amb aquest principi, no amb el de willUpdate, reservat a preparar dades derivades que el propi render() encara farà servir.

  2. Cridar this._calcularSiCercaDeVencer() directament dins de render() recalcularia aquest valor a cada execució de render(), incloses les que no tenen absolutament res a veure amb fechaLimite (per exemple, un canvi de prioridad o d'expandida), malbaratant feina en un càlcul que, la majoria de les vegades, donaria exactament el mateix resultat que ja tenia. A més, es perdria la possibilitat de reaccionar de forma selectiva només quan fechaLimite canvia de veritat (mitjançant changedProperties.has('fechaLimite')), que és exactament l'avantatge que aporta willUpdate en front de repetir el càlcul sense condició dins de la pròpia plantilla.

Conclusió

Aquesta lliçó ha completat el mapa del cicle d'actualització propi de Lit: shouldUpdate com a veto opcional, willUpdate per derivar estat abans de renderitzar, render() en el punt ja conegut, firstUpdated per allò que només ha de passar una vegada amb el DOM ja construït, updated per reaccionar a cada actualització ja pintada, i updateComplete com la promesa que permet a codi extern esperar que aquest cicle acabi. <task-card> ja usa willUpdate per recalcular la seva urgència per data en quant fechaLimite canvia, sense duplicar aquesta lògica dins de render(), complementant el temporitzador de la lliçó anterior en lloc de substituir-lo.

Tota aquesta lògica de temporitzador, però, viu encara directament dins de la classe TaskCard, mesclada amb la resta del seu comportament. Si TaskFlow necessités, més endavant, el mateix tipus d'avís per proximitat de data en un altre component diferent (per exemple, un futur resum de tasques urgents en el propi <task-board>), hauria de duplicar connectedCallback, disconnectedCallback i _calcularSiCercaDeVencer() per complet. La lliçó següent presenta l'eina que Lit ofereix precisament per evitar aquesta duplicació: els controladors reactius.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats