El módulo 5 ha cerrado con una pregunta pendiente: cada vez que estado cambia en <task-card> o tareas cambia en <task-board>, algo ocurre internamente en Lit para que la pantalla se actualice, pero este curso nunca se ha detenido a explicar ese "algo" con precisión. Antes de entrar en los hooks propios de Lit (contenido de la siguiente lección), conviene fijar primero una base más elemental: los callbacks del ciclo de vida que Lit hereda directamente del estándar de Custom Elements, sin añadir nada propio, y que ya se han mencionado de pasada en el módulo 1 sin desarrollarlos. Esta lección los explica en detalle y los aplica a un problema real de TaskFlow: avisar visualmente cuando una tarea está a punto de alcanzar su fechaLimite, usando un temporizador que hay que crear y destruir en el momento correcto.

Contenido

  1. Custom Elements ya tenía un ciclo de vida antes de que existiera Lit
  2. connectedCallback: cuándo entra un elemento en el DOM
  3. disconnectedCallback: cuándo sale, y por qué limpiar importa
  4. Por qué (casi) nunca hace falta sobreescribir el constructor
  5. Tabla comparativa: constructor, connectedCallback y disconnectedCallback
  6. Caso real: avisar cuando una tarea está cerca de su fecha límite
  7. Cierre: lo que falta por explicar

  1. Custom Elements ya tenía un ciclo de vida antes de que existiera Lit

Todo lo que se explica en esta lección no es una característica de Lit: es parte de la especificación estándar de Custom Elements, la misma API del navegador sobre la que Lit se construye, mencionada ya en la primera lección del curso. Cualquier clase que extienda HTMLElement (con o sin Lit de por medio) puede declarar hasta cuatro métodos especiales, con nombres reservados, que el navegador llama automáticamente en momentos concretos de la vida de un elemento personalizado: constructor, connectedCallback, disconnectedCallback y attributeChangedCallback (este último, gestionado casi siempre internamente por Lit para sincronizar atributos y propiedades, como se vio en el módulo 3, y rara vez hace falta tocarlo a mano).

Esta lección se centra en los tres primeros. Son "callbacks" en el sentido más literal: funciones que el propio navegador invoca por su cuenta, en respuesta a que el elemento se cree, se inserte en un documento o se elimine de él; el desarrollador no los llama nunca directamente, solo los sobreescribe para añadir código que deba ejecutarse en esos momentos concretos.

  1. connectedCallback: cuándo entra un elemento en el DOM

connectedCallback se ejecuta cada vez que el elemento se inserta en un documento con capacidad de renderizado (normalmente, el DOM de la página visible en el navegador). La palabra "cada vez" es deliberada: no es un evento que ocurra una única vez en la vida de un elemento, sino potencialmente varias, porque un elemento puede quitarse del DOM y volver a insertarse más adelante (por ejemplo, si algún código de la aplicación mueve un nodo de un contenedor a otro con appendChild), y cada una de esas inserciones dispara de nuevo connectedCallback.

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

El primer detalle que merece atención es la llamada a super.connectedCallback(). LitElement ya tiene su propia implementación de connectedCallback, que realiza trabajo interno imprescindible (entre otras cosas, programa la primera actualización del componente si todavía no se ha renderizado). Sobreescribir connectedCallback sin llamar a super.connectedCallback() primero rompería ese trabajo interno, así que la convención, sin excepción, es llamar siempre a super.connectedCallback() como primera línea del método, antes de añadir cualquier código propio.

connectedCallback es el lugar recomendado para cualquier inicialización que dependa de que el elemento esté realmente conectado a un documento: arrancar temporizadores, añadir listeners a objetos externos al propio componente (por ejemplo, a window o a document), abrir una conexión, o cualquier otro efecto que solo tenga sentido mientras el componente está visible y activo.

  1. disconnectedCallback: cuándo sale, y por qué limpiar importa

disconnectedCallback es el complemento exacto de connectedCallback: se ejecuta cada vez que el elemento se retira de un documento con capacidad de renderizado, ya sea porque se elimina definitivamente o porque, como se ha señalado en el apartado anterior, se mueve de un lugar a otro del DOM (una operación de movimiento se traduce, internamente, en una desconexión seguida de una reconexión).

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

La razón por la que este callback importa tanto en la práctica es la limpieza de recursos: todo lo que se active en connectedCallback y no dependa exclusivamente del propio ciclo de vida de Lit debe desactivarse explícitamente en disconnectedCallback. Un temporizador arrancado con setInterval en connectedCallback y nunca detenido seguirà ejecutándose indefinidamente, incluso después de que el elemento haya desaparecido del DOM y, en apariencia, "ya no exista"; mientras el intervalo siga activo, el motor de JavaScript mantiene viva una referencia al objeto (a través del cierre de la función que se ejecuta en cada tick), y ese objeto no puede liberarse de memoria. Esto es una fuga de memoria en el sentido más clásico del término, y es exactamente el tipo de error que disconnectedCallback existe para evitar.

Igual que con connectedCallback, la convención es llamar siempre a super.disconnectedCallback(), para no interferir con la limpieza interna que LitElement realiza por su cuenta.

  1. Por qué (casi) nunca hace falta sobreescribir el constructor

Quien llega a Lit desde otros lenguajes o frameworks orientados a objetos tiende a usar el constructor como el lugar natural para inicializar cualquier cosa. En Lit, sin embargo, el constructor tiene dos limitaciones importantes que hacen que, en la inmensa mayoría de los casos, no sea el sitio adecuado:

  • El elemento todavía no está conectado al DOM cuando se ejecuta el constructor. Un elemento personalizado puede crearse (por ejemplo, con document.createElement('task-card')) mucho antes de insertarse en ningún sitio, o incluso sin llegar a insertarse nunca. Cualquier inicialización que dependa de que el elemento esté realmente visible en una página —como el temporizador de esta lección— arrancaría en el momento equivocado si viviera en el constructor: podría arrancar para un elemento que jamás llega a mostrarse, y no existe ningún constructor simétrico que se ejecute al destruir el objeto para poder limpiarlo (a diferencia de disconnectedCallback, que sí existe para ese propósito).
  • Los valores por defecto de las propiedades reactivas ya cubren la inicialización simple. Todos los ejemplos de TaskFlow desde el módulo 3 inicializan titulo, estado, prioridad o fechaLimite dentro del constructor únicamente porque ahí es donde, por convención de JavaScript, se asignan valores a campos de instancia antes de que la clase esté lista; pero no hay ninguna lógica de ciclo de vida en juego en esas asignaciones, solo valores iniciales llanos. Cuando la inicialización es así de simple (this.estado = 'pendiente'), el constructor sigue siendo perfectamente adecuado; el problema aparece cuando esa inicialización implica efectos activos —temporizadores, suscripciones, listeners externos—, porque ahí sí hace falta esperar a connectedCallback.

La regla práctica que se sigue en TaskFlow, y que resume bien el criterio general de Lit, es esta: el constructor inicializa valores; connectedCallback inicia efectos. Si una propiedad solo necesita un valor inicial razonable, sigue yendo en el constructor. Si una operación implica algo que sigue "vivo" mientras el componente esté en pantalla (y que hay que apagar cuando deje de estarlo), pertenece a la pareja connectedCallback/disconnectedCallback, nunca al constructor en solitario.

  1. Tabla comparativa: constructor, connectedCallback y disconnectedCallback

Callback ¿Cuándo se ejecuta? ¿Cuántas veces? ¿El elemento está en el DOM? Uso típico
constructor Al crear la instancia de la clase Una sola vez en toda la vida del objeto No necesariamente Asignar valores iniciales a campos y propiedades
connectedCallback Al insertarse en un documento renderizable Una o más veces (cada inserción, incluidas las reinserciones) Arrancar temporizadores, suscripciones o listeners externos
disconnectedCallback Al retirarse de un documento renderizable Una vez por cada connectedCallback correspondiente Ya no (se acaba de retirar) Detener y liberar todo lo que se activó en connectedCallback

Esta tabla deja ver un principio de simetría que conviene interiorizar: cualquier cosa que se active dentro de connectedCallback debería tener su contraparte exacta, desactivándola, dentro de disconnectedCallback. Es la misma disciplina de "quien abre, cierra" que aparece en muchos otros contextos de programación (abrir y cerrar un fichero, adquirir y liberar un bloqueo), aplicada aquí al ciclo de vida de un componente.

  1. Caso real: avisar cuando una tarea está cerca de su fecha límite

TaskFlow ya tiene, desde el módulo 3, una propiedad fechaLimite en <task-card>, convertida a un objeto Date real gracias al conversor personalizado visto en aquel momento. Hasta ahora, esa fecha solo se mostraba como texto; esta lección la aprovecha para algo más útil: resaltar visualmente la tarjeta cuando falta menos de un día para que se cumpla.

Como el paso del tiempo no depende de que ninguna propiedad reactiva cambie (una tarea puede pasar de "lejos de vencer" a "cerca de vencer" sin que nadie modifique fechaLimite ni ninguna otra propiedad; simplemente pasan los minutos), no basta con recalcular esta situación cuando cambie algún dato: hace falta comprobarlo periódicamente con un temporizador, y ese temporizador es exactamente el tipo de "efecto activo" que pertenece 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();
    // Comprueba de inmediato, y a partir de ahí cada minuto,
    // si la tarea ha entrado en la ventana de "cerca de vencer".
    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>
    `;
  }
}

Varios detalles merecen comentario. Primero, cercaDeVencer se declara como estado interno ({ state: true }), no como propiedad pública: es un dato calculado dentro del propio componente a partir de fechaLimite y del reloj del sistema, no algo que ningún componente externo deba asignar directamente, exactamente el mismo criterio de expandida visto en el módulo 3. Segundo, _calcularSiCercaDeVencer es un método auxiliar corriente, sin nada especial de Lit, que aísla la lógica de cálculo del resto del código y facilita reutilizarla tanto en connectedCallback (para la comprobación inicial) como dentro del propio intervalo. Tercero, y más importante para esta lección: setInterval se arranca en connectedCallback, nunca en el constructor, precisamente porque no tendría sentido que una <task-card> creada pero nunca insertada en ninguna página mantuviera un temporizador corriendo indefinidamente sin ningún efecto visible; y se detiene con clearInterval en disconnectedCallback, para que, si la tarjeta se elimina de TaskFlow (por ejemplo, en un futuro ejercicio de eliminar tareas), el temporizador deje de ejecutarse y no queden referencias colgando en memoria.

El resultado, en la práctica de TaskFlow, es que cualquier tarjeta con una fechaLimite fijada para dentro de menos de 24 horas mostrará automáticamente el aviso "⏰ Está a punto de vencer", sin que nadie tenga que refrescar la página ni cambiar ninguna propiedad manualmente: el propio paso del tiempo, vigilado por el intervalo, hace que cercaDeVencer pase de false a true en el momento adecuado.

  1. Cierre: lo que falta por explicar

Con connectedCallback y disconnectedCallback ya dominados, <task-card> tiene su primer efecto real ligado al paso del tiempo, correctamente iniciado y correctamente limpiado. Pero estos dos callbacks, heredados del estándar de Custom Elements, no son los únicos puntos de enganche que ofrece Lit sobre el ciclo de una actualización: quedan por ver los hooks que Lit añade específicamente sobre su propio proceso de renderizado, capaces de responder no a "el elemento ha entrado o salido del DOM", sino a "una propiedad reactiva ha cambiado y se va a volver a ejecutar render()". Ese es el contenido de la siguiente lección.

Errores Comunes y Consejos

  • Olvidar super.connectedCallback() o super.disconnectedCallback(): sin esa llamada, se pierde el trabajo interno que LitElement realiza en esos mismos callbacks (entre otras cosas, la programación de la primera actualización), lo que puede producir errores sutiles y difíciles de rastrear, como un componente que nunca llega a renderizarse la primera vez.
  • Arrancar un temporizador, una suscripción o un listener externo en el constructor: como se explicó en el apartado 4, el constructor puede ejecutarse para elementos que nunca llegan a insertarse en el DOM, y no existe ningún callback simétrico al constructor para limpiar lo que allí se active; el lugar correcto es siempre connectedCallback, con su limpieza correspondiente en disconnectedCallback.
  • Olvidar disconnectedCallback por completo: es el error más habitual y el más costoso a largo plazo; cualquier setInterval, setTimeout recurrente, o addEventListener añadido sobre window o document (que no se limpia solo cuando el componente desaparece, a diferencia de los listeners puestos sobre el propio Shadow DOM del componente) debe desactivarse explícitamente, o la aplicación acumulará fugas de memoria cada vez que se creen y destruyan componentes.
  • Asumir que connectedCallback se ejecuta una sola vez en la vida del componente: como se ha explicado en el apartado 2, un elemento puede desconectarse y reconectarse varias veces (por ejemplo, al moverlo de un contenedor a otro); un connectedCallback mal escrito, que no comprueba si ya existe un intervalo previo antes de crear uno nuevo, podría acabar arrancando temporizadores duplicados si no se empareja correctamente con su disconnectedCallback.

Ejercicios

  1. Modifica _calcularSiCercaDeVencer() para que, en lugar de un único umbral de "menos de 24 horas", distinga tres niveles: lejos (más de 3 días), proxima (entre 3 días y 24 horas) y inminente (menos de 24 horas), guardando el resultado en una propiedad de estado urgenciaPorFecha de tipo String en lugar de un booleano, y ajusta render() para mostrar un mensaje distinto según el nivel.
  2. Explica, basándote en el apartado 4, por qué habría sido un error inicializar this._idIntervalo (o arrancar directamente el setInterval) dentro del constructor de <task-card>, incluso aunque fechaLimite ya tuviera un valor asignado en ese momento.
  3. Añade a <task-card> un console.log dentro de connectedCallback y otro dentro de disconnectedCallback, cada uno indicando el titulo de la tarea. Comprueba en el navegador, insertando y eliminando dinámicamente una <task-card> del DOM con JavaScript (por ejemplo, desde la consola con document.body.removeChild(...) y document.body.appendChild(...) sobre la misma referencia), que ambos mensajes se disparan en cada ciclo de inserción y retirada, no solo una vez.

Soluciones

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'; // ya venció; no tiene sentido seguir avisando
  }
  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);
}

En render(), un simple if/else encadenado (o un pequeño objeto de mensajes indexado por el valor de urgenciaPorFecha) sustituiría al aviso único de cercaDeVencer.

  1. Aunque fechaLimite ya tuviera un valor válido en el momento de ejecutarse el constructor, el problema no está en el valor de la propiedad, sino en si el elemento va a llegar a mostrarse alguna vez. Un document.createElement('task-card') seguido de una asignación de propiedades pero sin ningún appendChild posterior crearía una instancia con un setInterval corriendo indefinidamente en segundo plano, sin ningún disconnectedCallback que pudiera detenerlo jamás (porque ese callback solo se dispara si el elemento llegó a conectarse primero). connectedCallback garantiza que el temporizador solo existe mientras el elemento está realmente en un documento visible, y que tiene siempre un disconnectedCallback correspondiente capaz de limpiarlo.

  2. El resultado esperado es que cada llamada a appendChild sobre la referencia guardada dispare un nuevo connectedCallback (con su correspondiente mensaje de consola), y cada removeChild dispare un nuevo disconnectedCallback, tantas veces como se repita la operación. Esto confirma de forma práctica lo señalado en el apartado 2: estos callbacks no son eventos de "una sola vez en la vida del objeto", sino que se disparan en cada transición de conexión y desconexión del DOM, por eso la lógica de arranque y parada del intervalo debe vivir exactamente en esa pareja de callbacks y no en el constructor.

Conclusión

Esta lección ha explicado en detalle tres piezas del ciclo de vida de cualquier elemento personalizado —constructor, connectedCallback y disconnectedCallback— heredadas directamente del estándar de Custom Elements, sin nada específico de Lit de por medio. Se ha fijado la regla práctica de que el constructor inicializa valores simples, mientras que cualquier efecto activo (temporizadores, suscripciones, listeners externos) pertenece a la pareja connectedCallback/disconnectedCallback, con una limpieza simétrica y obligatoria para evitar fugas de memoria. <task-card> ya usa este patrón para avisar automáticamente cuando una tarea está cerca de su fechaLimite, sin necesidad de que ninguna otra propiedad cambie para disparar el aviso.

Quedan, sin embargo, los hooks que Lit añade sobre su propio proceso de actualización, capaces de reaccionar específicamente a que una propiedad reactiva haya cambiado: willUpdate, firstUpdated, updated y la promesa updateComplete. Esa es la siguiente pieza del módulo 6, y con ella se completará por fin la explicación de cuándo, exactamente, ocurre una actualización de Lit y en qué orden se ejecutan sus distintas fases.

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