El mòdul 5 ha tancat amb una pregunta pendent: cada vegada que estado canvia a <task-card> o tareas canvia a <task-board>, alguna cosa passa internament a Lit perquè la pantalla s'actualitzi, però aquest curs mai s'ha aturat a explicar aquest "alguna cosa" amb precisió. Abans d'entrar en els hooks propis de Lit (contingut de la lliçó següent), convé fixar primer una base més elemental: els callbacks del cicle de vida que Lit hereta directament de l'estàndard de Custom Elements, sense afegir-hi res propi, i que ja s'han esmentat de passada al mòdul 1 sense desenvolupar-los. Aquesta lliçó els explica en detall i els aplica a un problema real de TaskFlow: avisar visualment quan una tasca està a punt d'arribar a la seva fechaLimite, mitjançant un temporitzador que cal crear i destruir en el moment correcte.

Contingut

  1. Custom Elements ja tenia un cicle de vida abans que existís Lit
  2. connectedCallback: quan entra un element al DOM
  3. disconnectedCallback: quan surt, i per què importa netejar
  4. Per què (gairebé) mai cal sobreescriure el constructor
  5. Taula comparativa: constructor, connectedCallback i disconnectedCallback
  6. Cas real: avisar quan una tasca està a prop de la seva data límit
  7. Tancament: què queda per explicar

  1. Custom Elements ja tenia un cicle de vida abans que existís Lit

Tot el que s'explica en aquesta lliçó no és una característica de Lit: forma part de l'especificació estàndard de Custom Elements, la mateixa API del navegador sobre la qual Lit es construeix, ja esmentada a la primera lliçó del curs. Qualsevol classe que estengui HTMLElement (amb o sense Lit pel mig) pot declarar fins a quatre mètodes especials, amb noms reservats, que el navegador crida automàticament en moments concrets de la vida d'un element personalitzat: constructor, connectedCallback, disconnectedCallback i attributeChangedCallback (aquest últim, gestionat gairebé sempre internament per Lit per sincronitzar atributs i propietats, com es va veure al mòdul 3, i rares vegades cal tocar-lo a mà).

Aquesta lliçó se centra en els tres primers. Són "callbacks" en el sentit més literal: funcions que el propi navegador invoca pel seu compte, en resposta al fet que l'element es creï, s'insereixi en un document o se n'elimini; el desenvolupador no els crida mai directament, només els sobreescriu per afegir-hi codi que s'hagi d'executar en aquests moments concrets.

  1. connectedCallback: quan entra un element al DOM

connectedCallback s'executa cada vegada que l'element s'insereix en un document amb capacitat de renderitzat (normalment, el DOM de la pàgina visible al navegador). La paraula "cada vegada" és deliberada: no és un esdeveniment que passi una única vegada en la vida d'un element, sinó potencialment diverses, perquè un element es pot treure del DOM i tornar-s'hi a inserir més endavant (per exemple, si algun codi de l'aplicació mou un node d'un contenidor a un altre amb appendChild), i cada una d'aquestes insercions torna a disparar connectedCallback.

class TaskCard extends LitElement {
  connectedCallback() {
    super.connectedCallback();
    console.log('task-card insertada en el DOM');
  }
}

El primer detall que mereix atenció és la crida a super.connectedCallback(). LitElement ja té la seva pròpia implementació de connectedCallback, que fa una feina interna imprescindible (entre altres coses, programa la primera actualització del component si encara no s'ha renderitzat). Sobreescriure connectedCallback sense cridar primer super.connectedCallback() trencaria aquesta feina interna, així que la convenció, sense excepció, és cridar sempre super.connectedCallback() com a primera línia del mètode, abans d'afegir-hi cap codi propi.

connectedCallback és el lloc recomanat per a qualsevol inicialització que depengui del fet que l'element estigui realment connectat a un document: arrencar temporitzadors, afegir listeners a objectes externs al propi component (per exemple, a window o a document), obrir una connexió, o qualsevol altre efecte que només tingui sentit mentre el component estigui visible i actiu.

  1. disconnectedCallback: quan surt, i per què importa netejar

disconnectedCallback és el complement exacte de connectedCallback: s'executa cada vegada que l'element es retira d'un document amb capacitat de renderitzat, ja sigui perquè s'elimina definitivament o perquè, com s'ha assenyalat a l'apartat anterior, es mou d'un lloc a un altre del DOM (una operació de moviment es tradueix, internament, en una desconnexió seguida d'una reconnexió).

class TaskCard extends LitElement {
  disconnectedCallback() {
    super.disconnectedCallback();
    console.log('task-card retirada del DOM');
  }
}

La raó per la qual aquest callback importa tant a la pràctica és la neteja de recursos: tot el que s'activi a connectedCallback i no depengui exclusivament del propi cicle de vida de Lit s'ha de desactivar explícitament a disconnectedCallback. Un temporitzador arrencat amb setInterval a connectedCallback i mai aturat continuarà executant-se indefinidament, fins i tot després que l'element hagi desaparegut del DOM i, en aparença, "ja no existeixi"; mentre l'interval continuï actiu, el motor de JavaScript manté viva una referència a l'objecte (a través del tancament de la funció que s'executa a cada tick), i aquest objecte no es pot alliberar de memòria. Això és una fuga de memòria en el sentit més clàssic del terme, i és exactament el tipus d'error que disconnectedCallback existeix per evitar.

Igual que amb connectedCallback, la convenció és cridar sempre super.disconnectedCallback(), per no interferir amb la neteja interna que LitElement fa pel seu compte.

  1. Per què (gairebé) mai cal sobreescriure el constructor

Qui arriba a Lit des d'altres llenguatges o frameworks orientats a objectes tendeix a usar el constructor com el lloc natural per inicialitzar qualsevol cosa. A Lit, però, el constructor té dues limitacions importants que fan que, en la immensa majoria dels casos, no sigui el lloc adequat:

  • L'element encara no està connectat al DOM quan s'executa el constructor. Un element personalitzat es pot crear (per exemple, amb document.createElement('task-card')) molt abans d'inserir-se en cap lloc, o fins i tot sense arribar a inserir-se mai. Qualsevol inicialització que depengui del fet que l'element estigui realment visible en una pàgina —com el temporitzador d'aquesta lliçó— arrencaria en el moment equivocat si visqués al constructor: podria arrencar per a un element que mai arriba a mostrar-se, i no existeix cap constructor simètric que s'executi en destruir l'objecte per poder-lo netejar (a diferència de disconnectedCallback, que sí que existeix amb aquest propòsit).
  • Els valors per defecte de les propietats reactives ja cobreixen la inicialització simple. Tots els exemples de TaskFlow des del mòdul 3 inicialitzen titulo, estado, prioridad o fechaLimite dins del constructor únicament perquè aquí és on, per convenció de JavaScript, s'assignen valors a camps d'instància abans que la classe estigui llesta; però no hi ha cap lògica de cicle de vida en joc en aquestes assignacions, només valors inicials plans. Quan la inicialització és així de simple (this.estado = 'pendiente'), el constructor continua sent perfectament adequat; el problema apareix quan aquesta inicialització implica efectes actius —temporitzadors, subscripcions, listeners externs—, perquè aleshores sí que cal esperar a connectedCallback.

La regla pràctica que se segueix a TaskFlow, i que resumeix bé el criteri general de Lit, és aquesta: el constructor inicialitza valors; connectedCallback inicia efectes. Si una propietat només necessita un valor inicial raonable, continua anant al constructor. Si una operació implica alguna cosa que continua "viva" mentre el component estigui en pantalla (i que cal apagar quan deixi d'estar-ho), pertany a la parella connectedCallback/disconnectedCallback, mai al constructor en solitari.

  1. Taula comparativa: constructor, connectedCallback i disconnectedCallback

Callback Quan s'executa? Quantes vegades? L'element està al DOM? Ús típic
constructor En crear la instància de la classe Una sola vegada en tota la vida de l'objecte No necessàriament Assignar valors inicials a camps i propietats
connectedCallback En inserir-se en un document renderitzable Una o més vegades (cada inserció, incloses les reinsercions) Arrencar temporitzadors, subscripcions o listeners externs
disconnectedCallback En retirar-se d'un document renderitzable Una vegada per cada connectedCallback corresponent Ja no (s'acaba de retirar) Aturar i alliberar tot el que es va activar a connectedCallback

Aquesta taula deixa veure un principi de simetria que convé interioritzar: qualsevol cosa que s'activi dins de connectedCallback hauria de tenir la seva contrapartida exacta, desactivant-la, dins de disconnectedCallback. És la mateixa disciplina de "qui obre, tanca" que apareix en molts altres contextos de programació (obrir i tancar un fitxer, adquirir i alliberar un bloqueig), aplicada aquí al cicle de vida d'un component.

  1. Cas real: avisar quan una tasca està a prop de la seva data límit

TaskFlow ja té, des del mòdul 3, una propietat fechaLimite a <task-card>, convertida en un objecte Date real gràcies al conversor personalitzat vist en aquell moment. Fins ara, aquesta data només es mostrava com a text; aquesta lliçó l'aprofita per a alguna cosa més útil: ressaltar visualment la targeta quan falta menys d'un dia perquè es compleixi.

Com que el pas del temps no depèn del fet que cap propietat reactiva canviï (una tasca pot passar de "lluny de vèncer" a "a prop de vèncer" sense que ningú modifiqui fechaLimite ni cap altra propietat; simplement passen els minuts), no n'hi ha prou de recalcular aquesta situació quan canviï alguna dada: cal comprovar-ho periòdicament amb un temporitzador, i aquest temporitzador és exactament el tipus d'"efecte actiu" que pertany a connectedCallback/disconnectedCallback, no al constructor.

// src/components/task-card.js
class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    prioridad: { type: Number },
    urgente: { type: Boolean },
    expandida: { state: true },
    fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
    cercaDeVencer: { state: true },
  };

  constructor() {
    super();
    this.titulo = '';
    this.estado = 'pendiente';
    this.prioridad = 1;
    this.urgente = false;
    this.expandida = false;
    this.fechaLimite = null;
    this.cercaDeVencer = false;
  }

  connectedCallback() {
    super.connectedCallback();
    // Comprova immediatament, i a partir d'aquí cada minut,
    // si la tasca ha entrat en la finestra de "a prop de vèncer".
    this.cercaDeVencer = this._calcularSiCercaDeVencer();
    this._idIntervalo = setInterval(() => {
      this.cercaDeVencer = this._calcularSiCercaDeVencer();
    }, 60000);
  }

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

  _calcularSiCercaDeVencer() {
    if (!this.fechaLimite) {
      return false;
    }
    const unDiaEnMs = 24 * 60 * 60 * 1000;
    const msRestantes = this.fechaLimite.getTime() - Date.now();
    return msRestantes > 0 && msRestantes <= unDiaEnMs;
  }

  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>
    `;
  }
}

Diversos detalls mereixen comentari. Primer, cercaDeVencer es declara com a estat intern ({ state: true }), no com a propietat pública: és una dada calculada dins del propi component a partir de fechaLimite i del rellotge del sistema, no una cosa que cap component extern hagi d'assignar directament, exactament el mateix criteri de expandida vist al mòdul 3. Segon, _calcularSiCercaDeVencer és un mètode auxiliar corrent, sense res especial de Lit, que aïlla la lògica de càlcul de la resta del codi i facilita reutilitzar-la tant a connectedCallback (per a la comprovació inicial) com dins del propi interval. Tercer, i més important per a aquesta lliçó: setInterval s'arrenca a connectedCallback, mai al constructor, precisament perquè no tindria sentit que una <task-card> creada però mai inserida en cap pàgina mantingués un temporitzador corrent indefinidament sense cap efecte visible; i s'atura amb clearInterval a disconnectedCallback, perquè, si la targeta s'elimina de TaskFlow (per exemple, en un futur exercici d'eliminar tasques), el temporitzador deixi d'executar-se i no quedin referències penjades a la memòria.

El resultat, a la pràctica de TaskFlow, és que qualsevol targeta amb una fechaLimite fixada per a dins de menys de 24 hores mostrarà automàticament l'avís "⏰ Está a punto de vencer", sense que ningú hagi de refrescar la pàgina ni canviar cap propietat manualment: el propi pas del temps, vigilat per l'interval, fa que cercaDeVencer passi de false a true en el moment adequat.

  1. Tancament: què queda per explicar

Amb connectedCallback i disconnectedCallback ja dominats, <task-card> té el seu primer efecte real lligat al pas del temps, correctament iniciat i correctament netejat. Però aquests dos callbacks, heretats de l'estàndard de Custom Elements, no són els únics punts d'enganxament que ofereix Lit sobre el cicle d'una actualització: queden per veure els hooks que Lit afegeix específicament sobre el seu propi procés de renderitzat, capaços de respondre no a "l'element ha entrat o sortit del DOM", sinó a "una propietat reactiva ha canviat i es tornarà a executar render()". Aquest és el contingut de la lliçó següent.

Errors Comuns i Consells

  • Oblidar super.connectedCallback() o super.disconnectedCallback(): sense aquesta crida, es perd la feina interna que LitElement fa en aquests mateixos callbacks (entre altres coses, la programació de la primera actualització), la qual cosa pot produir errors subtils i difícils de rastrejar, com un component que mai arriba a renderitzar-se la primera vegada.
  • Arrencar un temporitzador, una subscripció o un listener extern al constructor: com s'ha explicat a l'apartat 4, el constructor pot executar-se per a elements que mai arriben a inserir-se al DOM, i no existeix cap callback simètric al constructor per netejar el que allà s'activi; el lloc correcte és sempre connectedCallback, amb la seva neteja corresponent a disconnectedCallback.
  • Oblidar disconnectedCallback completament: és l'error més habitual i el més costós a llarg termini; qualsevol setInterval, setTimeout recurrent, o addEventListener afegit sobre window o document (que no es neteja sol quan el component desapareix, a diferència dels listeners posats sobre el propi Shadow DOM del component) s'ha de desactivar explícitament, o l'aplicació acumularà fuites de memòria cada vegada que es creïn i destrueixin components.
  • Assumir que connectedCallback s'executa una sola vegada en la vida del component: com s'ha explicat a l'apartat 2, un element es pot desconnectar i reconnectar diverses vegades (per exemple, en moure'l d'un contenidor a un altre); un connectedCallback mal escrit, que no comprova si ja existeix un interval previ abans de crear-ne un de nou, podria acabar arrencant temporitzadors duplicats si no s'aparella correctament amb el seu disconnectedCallback.

Exercicis

  1. Modifica _calcularSiCercaDeVencer() perquè, en lloc d'un únic llindar de "menys de 24 hores", distingeixi tres nivells: lejos (més de 3 dies), proxima (entre 3 dies i 24 hores) i inminente (menys de 24 hores), guardant el resultat en una propietat d'estat urgenciaPorFecha de tipus String en lloc d'un booleà, i ajusta render() per mostrar un missatge diferent segons el nivell.
  2. Explica, basant-te en l'apartat 4, per què hauria estat un error inicialitzar this._idIntervalo (o arrencar directament el setInterval) dins del constructor de <task-card>, encara que fechaLimite ja tingués un valor assignat en aquell moment.
  3. Afegeix a <task-card> un console.log dins de connectedCallback i un altre dins de disconnectedCallback, cada un indicant el titulo de la tasca. Comprova al navegador, inserint i eliminant dinàmicament una <task-card> del DOM amb JavaScript (per exemple, des de la consola amb document.body.removeChild(...) i document.body.appendChild(...) sobre la mateixa referència), que ambdós missatges es disparen a cada cicle d'inserció i retirada, no només una vegada.

Solucions

static properties = {
  // ...resto de propiedades...
  urgenciaPorFecha: { state: true },
};

_calcularUrgenciaPorFecha() {
  if (!this.fechaLimite) {
    return 'lejos';
  }
  const unDiaEnMs = 24 * 60 * 60 * 1000;
  const msRestantes = this.fechaLimite.getTime() - Date.now();
  if (msRestantes <= 0) {
    return 'lejos'; // ja ha vençut; no té sentit continuar avisant
  }
  if (msRestantes <= unDiaEnMs) {
    return 'inminente';
  }
  if (msRestantes <= 3 * unDiaEnMs) {
    return 'proxima';
  }
  return 'lejos';
}

connectedCallback() {
  super.connectedCallback();
  this.urgenciaPorFecha = this._calcularUrgenciaPorFecha();
  this._idIntervalo = setInterval(() => {
    this.urgenciaPorFecha = this._calcularUrgenciaPorFecha();
  }, 60000);
}

A render(), un simple if/else encadenat (o un petit objecte de missatges indexat pel valor de urgenciaPorFecha) substituiria l'avís únic de cercaDeVencer.

  1. Encara que fechaLimite ja tingués un valor vàlid en el moment d'executar-se el constructor, el problema no és en el valor de la propietat, sinó en si l'element arribarà a mostrar-se algun cop. Un document.createElement('task-card') seguit d'una assignació de propietats però sense cap appendChild posterior crearia una instància amb un setInterval corrent indefinidament en segon pla, sense cap disconnectedCallback que el pogués aturar mai (perquè aquest callback només es dispara si l'element va arribar a connectar-se abans). connectedCallback garanteix que el temporitzador només existeix mentre l'element està realment en un document visible, i que té sempre un disconnectedCallback corresponent capaç de netejar-lo.

  2. El resultat esperat és que cada crida a appendChild sobre la referència guardada dispari un nou connectedCallback (amb el seu corresponent missatge de consola), i cada removeChild dispari un nou disconnectedCallback, tantes vegades com es repeteixi l'operació. Això confirma de manera pràctica el que s'ha assenyalat a l'apartat 2: aquests callbacks no són esdeveniments d'"una sola vegada en la vida de l'objecte", sinó que es disparen a cada transició de connexió i desconnexió del DOM, per això la lògica d'arrencada i aturada de l'interval ha de viure exactament en aquesta parella de callbacks i no al constructor.

Conclusió

Aquesta lliçó ha explicat en detall tres peces del cicle de vida de qualsevol element personalitzat —constructor, connectedCallback i disconnectedCallback— heretades directament de l'estàndard de Custom Elements, sense res específic de Lit pel mig. S'ha fixat la regla pràctica que el constructor inicialitza valors simples, mentre que qualsevol efecte actiu (temporitzadors, subscripcions, listeners externs) pertany a la parella connectedCallback/disconnectedCallback, amb una neteja simètrica i obligatòria per evitar fuites de memòria. <task-card> ja usa aquest patró per avisar automàticament quan una tasca està a prop de la seva fechaLimite, sense necessitat que cap altra propietat canviï per disparar l'avís.

Queden, però, els hooks que Lit afegeix sobre el seu propi procés d'actualització, capaços de reaccionar específicament al fet que una propietat reactiva hagi canviat: willUpdate, firstUpdated, updated i la promesa updateComplete. Aquesta és la peça següent del mòdul 6, i amb ella es completarà finalment l'explicació de quan, exactament, passa una actualització de Lit i en quin ordre s'executen les seves diferents fases.

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