En la lección anterior creaste tu primer componente Lit sin detenerte demasiado en el porqué de cada pieza. Antes de avanzar hacia las plantillas reactivas, conviene hacer una pausa y entender con detalle la anatomía completa de una clase que extiende LitElement: qué partes la componen, cómo se relaciona con el estándar de Custom Elements que ya conoces, qué implica que use Shadow DOM por defecto, y qué momentos de vida atraviesa un componente desde que nace hasta que desaparece. Esta lección es la última teórica del módulo y cierra el círculo abierto en la primera lección, dejando el terreno preparado para el módulo 2, donde empezarás a trabajar con plantillas realmente dinámicas.

Contenido

  1. Estructura general de una clase LitElement
  2. Declarar propiedades: decoradores frente a static properties
  3. LitElement es HTMLElement: la relación con el estándar
  4. Shadow DOM por defecto: qué significa y qué implica
  5. Panorama del ciclo de vida de un componente Lit
  6. Hacia el módulo 2: plantillas reactivas

  1. Estructura general de una clase LitElement

Toda clase que extiende LitElement puede contener, en general, los siguientes bloques (ninguno es obligatorio salvo render(), que es el único imprescindible para que el componente muestre algo):

import { LitElement, html, css } from 'lit';

class TaskCard extends LitElement {

  // 1. Declaración de propiedades reactivas (se detalla en el módulo 3)
  static properties = {
    titulo: { type: String },
  };

  // 2. Estilos encapsulados (se detallan en el módulo 4)
  static styles = css`
    :host {
      display: block;
      border: 1px solid #ccc;
    }
  `;

  // 3. Constructor: inicialización del estado interno
  constructor() {
    super();
    this.titulo = 'Tarea sin título';
  }

  // 4. Callbacks del ciclo de vida (se detallan en el módulo 6)
  connectedCallback() {
    super.connectedCallback();
  }

  // 5. Plantilla del componente (obligatorio)
  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}

customElements.define('task-card', TaskCard);

No te preocupes si algunas de estas piezas (static properties, static styles, connectedCallback) todavía no tienen mucho sentido: se explicarán en profundidad en los módulos 3, 4 y 6 respectivamente. El objetivo de este repaso es que, al llegar a esos módulos, ya tengas clara la "forma" general de la clase y sepas ubicar cada pieza nueva dentro de un esquema conocido. De hecho, fíjate en que el componente task-card.js de la lección anterior era, simplemente, una versión de esta misma estructura en la que solo se rellenaba el bloque 5 (render()); todos los demás bloques son opcionales y se añaden según las necesidades del componente.

Un detalle sobre el constructor: si se define, es obligatorio llamar primero a super(), como en cualquier clase de JavaScript que hereda de otra. Es el lugar habitual para inicializar valores por defecto de las propiedades del componente, algo que se retomará con más detalle en el módulo 3.

  1. Declarar propiedades: decoradores frente a static properties

En la lección anterior ya viste que existen dos estilos para registrar un componente: customElements.define y el decorador @customElement. Esa misma dualidad de estilos se repite para declarar las propiedades reactivas de un componente (su estudio completo corresponde al módulo 3, pero conviene reconocer ya la sintaxis, porque aparecerá en ejemplos de este curso y en la documentación oficial de Lit).

Con decoradores (requiere soporte de compilación, como se explicó en la lección de configuración del entorno):

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('task-card')
class TaskCard extends LitElement {
  @property({ type: String })
  titulo = 'Tarea sin título';

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}

Con static properties, en JavaScript plano sin ningún paso de compilación adicional:

import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
  };

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
  }

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}

customElements.define('task-card', TaskCard);
Aspecto Con decoradores (@property) Con static properties
Requiere compilación adicional Sí (TypeScript o Babel con plugin de decoradores) No, JavaScript estándar
Dónde se declara el valor por defecto Junto a la propia declaración de la propiedad En el constructor, tras super()
Verbosidad Más compacta Ligeramente más extensa
Resultado en tiempo de ejecución Idéntico Idéntico

Ambos estilos producen exactamente el mismo comportamiento; la elección es una cuestión de preferencia y de las herramientas disponibles en el proyecto. Este curso, como ya se explicó en la lección de configuración del entorno, usará principalmente static properties, mostrando la variante con decoradores como referencia cuando resulte útil para quien trabaje con TypeScript en sus propios proyectos.

  1. LitElement es HTMLElement: la relación con el estándar

Es fundamental interiorizar una idea que ya se apuntó en la primera lección del módulo: LitElement no sustituye el estándar de Custom Elements, lo extiende. En términos de JavaScript, la cadena de herencia es:

HTMLElement  →  ReactiveElement  →  LitElement  →  TaskCard (tu componente)
  • HTMLElement es la clase base de la que heredan todos los elementos del DOM, tanto los nativos del navegador (HTMLButtonElement, HTMLDivElement...) como cualquier Custom Element.
  • ReactiveElement es una clase intermedia que Lit añade para incorporar el sistema de propiedades reactivas y el ciclo de actualización, sin todavía imponer un motor concreto de plantillas.
  • LitElement hereda de ReactiveElement y añade el método render() junto con el uso de la función html como motor de plantillas.
  • Tu clase (TaskCard, en el ejemplo) hereda de LitElement y hereda, por tanto, todas las capacidades de las tres clases anteriores.

La consecuencia práctica de esta cadena de herencia es que un componente Lit es, literalmente, un HTMLElement. Esto significa que hereda toda la API estándar de un elemento del DOM: se le pueden añadir y quitar atributos (setAttribute, getAttribute), se le pueden escuchar y despachar eventos (addEventListener, dispatchEvent, algo que se explota a fondo en el módulo de eventos), tiene acceso a classList, y participa en el árbol del DOM exactamente igual que cualquier otro elemento. Nada de esto es una simulación ni una capa aparte: es la propia plataforma web.

  1. Shadow DOM por defecto: qué significa y qué implica

En la lección anterior, al inspeccionar <task-card> en las herramientas de desarrollo, apareció un nodo #shadow-root que quizá resultó sorprendente. Esto ocurre porque LitElement, por defecto, llama internamente a attachShadow({ mode: 'open' }) sobre cada instancia del componente y renderiza el resultado de render() dentro de ese Shadow DOM, no directamente como hijos del elemento en el DOM normal.

// Esto es, de forma simplificada, lo que hace LitElement por ti
// la primera vez que el componente necesita renderizarse:
const raizSombra = this.attachShadow({ mode: 'open' });
raizSombra.innerHTML = /* resultado de render() */;

¿Qué implica esto en la práctica, sin entrar todavía en el detalle de los estilos (que corresponde al módulo 4)?

  • Encapsulación del HTML interno: el contenido devuelto por render() no es visible mediante document.querySelector desde fuera del componente; hay que acceder primero a elemento.shadowRoot y buscar dentro de él.
  • Encapsulación de los estilos: cualquier CSS que se declare dentro del componente (con static styles, como se vio en el apartado 1) no se filtra hacia el resto de la página, y los estilos globales de la página tampoco entran por accidente en el componente. Esto evita uno de los problemas más habituales al construir interfaces grandes: colisiones de nombres de clases CSS entre componentes distintos.
  • Contenido distribuido con <slot>: el Shadow DOM también define un mecanismo, <slot>, para permitir que un componente reciba contenido HTML "de fuera" y lo posicione dentro de su propia estructura interna. Este mecanismo se explorará en detalle en el módulo de estilos.

Por ahora, el punto clave que hay que retener es este: cada componente Lit vive, por defecto, en su propia burbuja aislada del resto de la página, tanto en estructura como en estilos. Esta es una de las razones por las que Lit resulta especialmente adecuado para construir bibliotecas de componentes reutilizables como los de TaskFlow: se pueden combinar <task-card>, <task-list> y <user-avatar> sin miedo a que los estilos de uno interfieran con los de otro.

  1. Panorama del ciclo de vida de un componente Lit

Un componente Lit, igual que cualquier Custom Element, no existe de forma estática: nace, se actualiza posiblemente muchas veces, y en algún momento puede desaparecer del DOM. Lit ofrece una serie de métodos ("callbacks del ciclo de vida") que se ejecutan automáticamente en distintos momentos de esa vida, y que un componente puede sobrescribir para ejecutar lógica propia en el momento adecuado.

En esta lección solo se presenta un mapa general, con el nombre de cada callback y para qué sirve a grandes rasgos; el funcionamiento detallado, con ejemplos de uso real, se estudia en profundidad en el módulo 6 ("Ciclo de Vida y Comportamiento Avanzado").

Momento del ciclo de vida Callback Para qué sirve, a grandes rasgos
El elemento se inserta en el DOM connectedCallback() Heredado directamente del estándar de Custom Elements; buen lugar para suscribirse a eventos externos o iniciar temporizadores
El elemento se retira del DOM disconnectedCallback() También del estándar; lugar habitual para "limpiar" lo que se inició en connectedCallback (cancelar suscripciones, temporizadores...)
Antes de aplicar una actualización willUpdate(changedProperties) Propio de Lit; permite calcular valores derivados justo antes de renderizar, en función de qué propiedades han cambiado
Se genera la nueva plantilla render() El único obligatorio; devuelve el HTML actual del componente
Después de aplicar una actualización al DOM updated(changedProperties) Propio de Lit; útil para reaccionar a cambios ya reflejados en el DOM real
Justo después de la primera actualización firstUpdated(changedProperties) Propio de Lit; se ejecuta una única vez, ideal para inicializaciones que necesitan que el DOM interno ya exista

Dos ideas importantes para quedarse, aunque el detalle llegue más adelante:

  • Los callbacks connectedCallback y disconnectedCallback no son una invención de Lit: pertenecen al estándar de Custom Elements visto en la primera lección del módulo. Lit los aprovecha y añade su propia capa de callbacks adicionales (willUpdate, updated, firstUpdated) específicos de su sistema de renderizado reactivo.
  • Ningún componente necesita implementar todos estos callbacks. La mayoría de los componentes sencillos, como el <task-card> estático de la lección anterior, no necesitan ninguno de ellos; se van incorporando a medida que el componente necesita reaccionar a momentos concretos de su vida.

  1. Hacia el módulo 2: plantillas reactivas

Con esta lección se cierra el módulo de introducción. Ya conoces la base sobre la que se apoya todo lo demás: qué son los Web Components como estándar, qué aporta Lit sobre ellos, cómo configurar un entorno de trabajo, cómo se ve por dentro una clase LitElement, y qué momentos de vida atraviesa un componente.

Sin embargo, todos los ejemplos vistos hasta ahora comparten una limitación deliberada: sus plantillas son completamente estáticas. El <task-card> que has construido siempre muestra la misma tarea de ejemplo, sin importar qué ocurra en la aplicación. Esa limitación es exactamente el punto de partida del módulo 2, "Plantillas Reactivas y Renderizado", donde aprenderás a insertar expresiones dinámicas dentro de la función html, a mostrar u ocultar contenido según condiciones, a renderizar listas completas de tareas, y a entender cómo y cuándo decide Lit volver a ejecutar render().

Errores Comunes y Consejos

  • Intentar acceder al contenido interno del componente con document.querySelector desde fuera: como el contenido vive dentro de un Shadow DOM, document.querySelector('task-card h3') no encontrará nada. Hay que acceder primero al propio elemento y luego a su shadowRoot (elemento.shadowRoot.querySelector('h3')), o mejor aún, evitar manipular el DOM interno manualmente y dejar que sea render() quien decida qué se muestra.
  • Olvidar llamar a super() en el constructor: si se define un constructor personalizado sin invocar primero a super(), el navegador lanzará un error en tiempo de ejecución. Esta regla no es específica de Lit, sino de cualquier clase de JavaScript que herede de otra.
  • Pensar que hay que implementar todos los callbacks del ciclo de vida: como se ha visto en la tabla del apartado 5, la mayoría de los componentes solo necesitan render(). Añadir callbacks "por si acaso" complica el componente sin necesidad.
  • Confundir ReactiveElement con LitElement: en el día a día casi nunca se interactúa directamente con ReactiveElement; se menciona aquí solo para entender la cadena de herencia. En la práctica, todos los componentes de este curso extenderán LitElement directamente.

Ejercicios

  1. Dibuja (en un papel o en un fichero de texto) la cadena de herencia completa de un componente UserAvatar que definas tú mismo, desde HTMLElement hasta tu clase, indicando qué aporta cada eslabón de la cadena.
  2. Retoma el componente task-card.js de la lección anterior (la versión estática) y añade un método connectedCallback() que, mediante console.log, escriba en la consola del navegador el mensaje "task-card conectado al DOM". Recuerda llamar a super.connectedCallback().
  3. Explica con tus propias palabras, en dos o tres frases, por qué el hecho de que LitElement use Shadow DOM por defecto es una ventaja para un proyecto como TaskFlow, que tendrá varios componentes distintos conviviendo en la misma página.

Soluciones

  1. La cadena, igual que la de TaskCard vista en el apartado 3, es:
HTMLElement (estándar del navegador: API base de cualquier elemento del DOM)
  → ReactiveElement (Lit: sistema de propiedades reactivas y ciclo de actualización)
    → LitElement (Lit: motor de plantillas mediante render() y html)
      → UserAvatar (tu clase: la lógica y plantilla concretas del avatar)
import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  connectedCallback() {
    super.connectedCallback();
    console.log('task-card conectado al DOM');
  }

  render() {
    return html`
      <article>
        <h3>Preparar la demo del sprint</h3>
        <p>Estado: En curso</p>
        <p>Asignada a: Ana</p>
      </article>
    `;
  }
}

customElements.define('task-card', TaskCard);

Al recargar la página con las herramientas de desarrollo abiertas, debería aparecer el mensaje en la consola por cada <task-card> presente en el HTML.

  1. Una posible respuesta: al estar cada componente encapsulado en su propio Shadow DOM, los estilos CSS de <task-card>, <task-list> o <user-avatar> no pueden interferir entre sí ni con los estilos globales de la página, aunque usen los mismos nombres de clase o de etiqueta. Esto permite desarrollar y mantener cada componente de forma independiente, sin necesidad de coordinar convenciones de nombres CSS entre todo el equipo, algo especialmente valioso a medida que TaskFlow vaya sumando más componentes a lo largo del curso.

Conclusión

En esta lección has terminado de desmontar la anatomía de un componente Lit: su estructura general con bloques opcionales y un único método obligatorio (render()), las dos formas de declarar propiedades (decoradores y static properties), su relación directa con HTMLElement a través de la cadena de herencia HTMLElement → ReactiveElement → LitElement, el uso del Shadow DOM por defecto para encapsular estructura y estilos, y una primera panorámica —solo los nombres y su propósito general— del ciclo de vida que se detallará en el módulo 6.

Con esto se cierra el módulo de introducción. En el módulo 2, "Plantillas Reactivas y Renderizado", dejarás atrás los componentes estáticos: aprenderás a insertar expresiones dinámicas en la función html, a renderizar condicionalmente contenido, a mostrar listas de elementos y a entender en profundidad el ciclo de renderizado de Lit, dando a <task-card>, <task-list> y el resto de piezas de TaskFlow su primera capa real de vida dinámica.

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