En la última lección del módulo anterior quedó identificado el problema con precisión: <task-card> y <task-list> usan campos de instancia normales (this.titulo, this.estado, this.tareas...), y esos campos no disparan ninguna actualización porque Lit no tiene ningún mecanismo instalado para vigilarlos. Esta lección resuelve exactamente esa carencia: aprenderás a declarar propiedades reactivas de verdad con static properties, entenderás qué ocurre por dentro cuando se declara una propiedad de esta forma, y convertirás <task-card> para que titulo, estado, prioridad y urgente dejen de ser campos sueltos y pasen a ser propiedades reactivas reconocidas por Lit.

Contenido

  1. Qué significa exactamente que una propiedad sea "reactiva"
  2. Declarar propiedades con static properties
  3. Las opciones de configuración: type, attribute, reflect
  4. La alternativa con el decorador @property
  5. Convirtiendo <task-card> a propiedades reactivas reales
  6. Usar <task-card> desde HTML (atributos) y desde JavaScript (propiedades)
  7. Qué pasa por dentro: el mecanismo de los accessors

  1. Qué significa exactamente que una propiedad sea "reactiva"

En el apartado 1 de la última lección del módulo 2 se explicó que render() no se llama solo: necesita un disparador, y que ese disparador habitual es un cambio en una propiedad reactiva. Ahora toca precisar qué significa exactamente esa palabra, "reactiva", aplicada a una propiedad de un componente Lit.

Una propiedad reactiva es una propiedad de la clase que Lit conoce explícitamente porque se ha declarado como tal (con static properties, o con el decorador @property, que se verá en el apartado 4). Al declararla, Lit instala sobre ella un mecanismo especial —un getter y un setter, como se detallará en el apartado 7— que le permite detectar automáticamente cuándo cambia su valor. En el momento en que ese cambio se detecta, Lit programa una actualización del componente, siguiendo exactamente el proceso asíncrono y agrupado que se explicó en la lección del ciclo de renderizado.

La diferencia con un campo de instancia normal, como los que se han usado durante todo el módulo 2, es justamente esa: un campo de instancia (this.titulo = 'algo' sin más) es una asignación de JavaScript corriente, invisible para Lit. Una propiedad reactiva es una asignación que pasa por un mecanismo de Lit capaz de reaccionar a ella. El nombre de la propiedad puede ser idéntico en ambos casos (this.titulo); lo que cambia es si esa propiedad ha sido declarada de antemano en static properties (o con @property) o no.

  1. Declarar propiedades con static properties

La forma más directa de declarar propiedades reactivas, sin necesidad de ningún paso de compilación adicional (como ya se adelantó en la lección de anatomía del módulo 1), es un campo estático de la clase llamado properties, que contiene un objeto: una clave por cada propiedad que se quiere declarar, y un valor que describe cómo debe comportarse esa propiedad.

import { LitElement, html } from 'lit';

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

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

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

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

Dos detalles importantes en este ejemplo:

  • static properties se declara una vez, fuera de cualquier método, como un campo estático de la clase (con la sintaxis moderna de campos de clase de JavaScript, disponible sin transpilación en cualquier navegador moderno). No es un método, ni se ejecuta cada vez que cambia algo: es simplemente la lista de "qué propiedades existen y cómo son", que Lit lee una única vez, al definir la clase.
  • El valor inicial de cada propiedad se sigue asignando en el constructor, exactamente igual que con los campos de instancia del módulo anterior. static properties no asigna ningún valor por sí sola: solo declara la existencia de la propiedad y su configuración. Es responsabilidad del constructor (tras la llamada obligatoria a super()) darle un valor inicial razonable.

A partir de este momento, this.titulo y this.estado ya no son campos de instancia corrientes: son propiedades reactivas. Cualquier asignación posterior (elemento.titulo = 'Otro título', ya sea desde dentro de la clase o desde fuera) pasará por el mecanismo de Lit y disparará una actualización.

  1. Las opciones de configuración: type, attribute, reflect

El objeto de configuración de cada propiedad ({ type: String } en los ejemplos anteriores) admite varias claves. Las tres más relevantes en este punto del curso son type, attribute y reflect; las dos primeras se ven aquí con detalle, y reflect se retoma en profundidad en la lección "Atributos vs Propiedades y Reflexión" de este mismo módulo, así que aquí basta con una introducción.

Opción Para qué sirve Valor por defecto
type Indica a Lit qué tipo de dato tiene la propiedad (String, Number, Boolean, Array, Object), para poder convertir correctamente entre el atributo HTML (siempre texto) y el valor JavaScript de la propiedad String si no se indica
attribute Controla si la propiedad tiene un atributo HTML asociado, y con qué nombre. Por defecto, Lit crea automáticamente un atributo con el mismo nombre que la propiedad, en minúsculas true (mismo nombre, en minúsculas)
reflect Controla si, además de leer el atributo hacia la propiedad, Lit también escribe de vuelta el atributo cuando cambia la propiedad desde JavaScript false

Un ejemplo con las tres opciones a la vez, sobre una propiedad ficticia prioridadAlta:

static properties = {
  prioridadAlta: {
    type: Boolean,
    attribute: 'prioridad-alta', // el atributo HTML se llamará "prioridad-alta", no "prioridadalta"
    reflect: true,               // cambiar la propiedad desde JS actualiza también el atributo
  },
};

Fíjate en el nombre del atributo: como HTML no distingue mayúsculas de minúsculas en los nombres de atributo, una propiedad con nombre en camelCase como prioridadAlta necesitaría, si se dejara el valor por defecto, convertirse a prioridadalta (todo junto, en minúsculas), lo cual es difícil de leer. Por eso, en propiedades con nombres compuestos, es habitual indicar explícitamente el nombre de atributo deseado en formato kebab-case (prioridad-alta), como se ha hecho aquí.

Si no interesa que una propiedad tenga ningún atributo asociado —por ejemplo, porque solo tiene sentido asignarla desde JavaScript, nunca desde HTML—, se puede desactivar por completo con attribute: false. En ese caso, la propiedad sigue siendo reactiva (Lit sigue vigilando sus cambios desde JavaScript), pero no existe forma de establecer su valor inicial mediante un atributo HTML.

  1. La alternativa con el decorador @property

Como ya se apuntó en la lección de anatomía del módulo 1, Lit ofrece una sintaxis alternativa, equivalente en comportamiento, basada en decoradores de TypeScript o de JavaScript con Babel:

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';

  @property({ type: String })
  estado = 'pendiente';

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

El decorador @property recibe exactamente el mismo objeto de configuración que una entrada de static properties (type, attribute, reflect), y el valor inicial se asigna directamente junto a la declaración del campo, sin necesidad de un constructor explícito. El resultado en tiempo de ejecución es idéntico al de static properties: ambas sintaxis terminan generando el mismo mecanismo interno de propiedades reactivas.

Este curso sigue usando static properties como estilo principal, por no requerir ningún paso de compilación adicional (tal como se explicó en la lección de configuración del entorno del módulo 1), pero es importante reconocer la sintaxis con decoradores porque aparece con mucha frecuencia en la documentación oficial de Lit y en proyectos reales que usan TypeScript.

  1. Convirtiendo <task-card> a propiedades reactivas reales

Con la teoría ya cubierta, es el momento de dar el paso que se anunció al cerrar el módulo 2: convertir los campos de instancia de <task-card> (titulo, estado, prioridad, urgente) en propiedades reactivas declaradas con static properties. Retoma el fichero src/components/task-card.js tal como quedó al final del módulo 2, con la insignia de estado y el aviso de urgencia, y añade la declaración de propiedades:

import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    prioridad: { type: Number },
    urgente: { type: Boolean },
  };

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
    this.estado = 'pendiente'; // 'pendiente' | 'en-progreso' | 'hecha'
    this.prioridad = 3;
    this.urgente = false;
  }

  renderInsigniaEstado() {
    if (this.estado === 'hecha') {
      return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
    }
    if (this.estado === 'en-progreso') {
      return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
    }
    return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
  }

  render() {
    return html`
      <article>
        <h3>${this.titulo}</h3>
        ${this.renderInsigniaEstado()}
        <p>Prioridad: ${this.prioridad}</p>
        ${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
      </article>
    `;
  }
}

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

Compara este código con el de la lección "Renderizado Condicional" del módulo 2: la plantilla (render(), renderInsigniaEstado()) no ha cambiado ni una línea. Lo único nuevo es el bloque static properties, que declara explícitamente las cuatro propiedades, y la incorporación de prioridad a la plantilla (antes no se mostraba en ningún sitio, ya que no tenía sentido enseñar un valor que no podía cambiar de forma reactiva). Esta es una idea que conviene retener: añadir reactividad no exige reescribir la plantilla; render() sigue leyendo this.titulo, this.estado, etc., exactamente igual que antes. Lo que cambia es que, ahora, escribir en esas propiedades sí que provoca que render() se vuelva a ejecutar.

  1. Usar <task-card> desde HTML (atributos) y desde JavaScript (propiedades)

Con las propiedades ya declaradas, <task-card> puede recibir sus valores de dos formas distintas, y es importante distinguirlas con claridad.

Desde HTML, mediante atributos (siempre como texto, por eso solo tiene sentido para titulo y estado, cuyo tipo declarado es String; para prioridad, de tipo Number, y urgente, de tipo Boolean, Lit se encarga de convertir el texto del atributo al tipo declarado, como se detallará en la siguiente lección):

<task-card titulo="Preparar la demo del sprint" estado="en-progreso"></task-card>

Desde JavaScript, mediante la propiedad del DOM (con cualquier tipo de dato, sin pasar por texto):

const tarjeta = document.querySelector('task-card');
tarjeta.titulo = 'Revisar el PR de autenticación';
tarjeta.estado = 'pendiente';
tarjeta.prioridad = 5;
tarjeta.urgente = true;

Y, como ya se vio en la lección de expresiones e interpolación del módulo 2, también se puede asignar una propiedad directamente dentro de una plantilla html usando el prefijo de punto:

html`<task-card .titulo="${'Desplegar a producción'}" .prioridad="${5}"></task-card>`

Con las propiedades declaradas como reactivas, cualquiera de estas tres formas de asignar un valor —atributo HTML al crear el elemento, propiedad de JavaScript en cualquier momento, o interpolación con punto dentro de una plantilla html— dispara ahora una actualización real: la próxima vez que Lit ejecute render(), la tarjeta mostrará los nuevos valores. Esta es, exactamente, la pieza que faltaba durante todo el módulo 2.

  1. Qué pasa por dentro: el mecanismo de los accessors

Para terminar de desmitificar la "magia" de las propiedades reactivas, conviene entender, a un nivel razonable de detalle, qué hace Lit realmente cuando procesa static properties. Por cada propiedad declarada, Lit instala sobre el prototipo de la clase un par de funciones especiales de JavaScript llamadas getter y setter (mediante Object.defineProperty, una funcionalidad estándar del lenguaje, no específica de Lit), en lugar de dejar que titulo sea un campo normal.

De forma simplificada, es como si Lit generase automáticamente algo equivalente a esto para la propiedad titulo:

// Simplificación de lo que Lit hace internamente por cada propiedad declarada
get titulo() {
  return this._titulo;
}

set titulo(valorNuevo) {
  const valorAnterior = this._titulo;
  this._titulo = valorNuevo;
  this.requestUpdate('titulo', valorAnterior); // el disparador visto en el módulo 2
}

Cuando en el código se escribe this.titulo = 'Nuevo título', en realidad no se está asignando directamente ningún campo: se está llamando al setter que Lit ha instalado, el cual guarda el valor nuevo en un campo interno, y a continuación llama a this.requestUpdate(...), exactamente el mismo método de bajo nivel que se presentó en la última lección del módulo 2. Esto confirma la idea que se anticipó entonces: las propiedades reactivas no son un mecanismo distinto de requestUpdate(), sino una capa de comodidad construida encima de él, que lo invoca automáticamente en el momento adecuado.

Este detalle interno no hace falta memorizarlo para trabajar con Lit en el día a día, pero conocerlo ayuda a razonar sobre casos menos obvios: por ejemplo, explica por qué mutar un array o un objeto guardado en una propiedad reactiva (this.tareas.push(nuevaTarea)) no dispara ninguna actualización por sí solo, mientras que reasignar la propiedad entera sí lo hace (this.tareas = [...this.tareas, nuevaTarea]): el setter solo se ejecuta cuando hay una asignación real a this.tareas, no cuando se llama a un método que modifica el contenido del array ya existente sin volver a asignarlo. Esta distinción se retomará con detalle cuando en el curso se trabaje con colecciones que cambian con el tiempo.

Errores Comunes y Consejos

  • Olvidar declarar una propiedad en static properties y esperar que sea reactiva: si se asigna this.algo = valor sin haber declarado algo en static properties (ni con @property), esa asignación es un campo de instancia normal, invisible para Lit, exactamente como se explicó en el módulo 2. El síntoma es siempre el mismo: el valor cambia internamente, pero la pantalla no se actualiza.
  • Asignar el valor inicial dentro de static properties en lugar de en el constructor: static properties solo declara la configuración de la propiedad (tipo, atributo, reflexión); no es el lugar para poner el valor por defecto. El valor inicial se asigna en el constructor, tras super(), como en todos los ejemplos de esta lección.
  • Mutar un array o un objeto reactivo esperando que dispare una actualización: como se explicó en el apartado 7, this.coleccion.push(elemento) no pasa por el setter de la propiedad, así que Lit no lo detecta. Hay que reasignar la propiedad completa (por ejemplo, con el operador de propagación [...this.coleccion, elemento]) para que el cambio sea visible para Lit.
  • Confundir nombre de propiedad con nombre de atributo en propiedades con camelCase: como se vio en el apartado 3, una propiedad prioridadAlta sin configuración explícita de attribute genera por defecto un atributo prioridadalta (todo junto), no prioridad-alta. Si se quiere un atributo en kebab-case, hay que indicarlo explícitamente.

Ejercicios

  1. Añade a <task-card> una nueva propiedad reactiva llamada asignadoA, de tipo String, con valor inicial 'Sin asignar', y muéstrala en la plantilla dentro de un párrafo <p>Asignada a: ${this.asignadoA}</p>. Comprueba, desde la consola del navegador, que elemento.asignadoA = 'Ana' actualiza la pantalla.
  2. Declara en <task-card> una propiedad completadaEn, de tipo String, cuyo atributo HTML se llame explícitamente completada-en (usando la opción attribute vista en el apartado 3). Escribe el HTML de ejemplo que usarías para establecer esa propiedad desde una etiqueta <task-card>.
  3. Basándote en el apartado 7, explica con tus propias palabras por qué this.tareas.push(nuevaTarea) sobre una propiedad reactiva tareas de tipo Array no provoca ninguna actualización visible, y qué línea de código habría que escribir en su lugar para que sí la provocara.

Soluciones

static properties = {
  titulo: { type: String },
  estado: { type: String },
  prioridad: { type: Number },
  urgente: { type: Boolean },
  asignadoA: { type: String },
};

constructor() {
  super();
  this.titulo = 'Tarea sin título';
  this.estado = 'pendiente';
  this.prioridad = 3;
  this.urgente = false;
  this.asignadoA = 'Sin asignar';
}

render() {
  return html`
    <article>
      <h3>${this.titulo}</h3>
      ${this.renderInsigniaEstado()}
      <p>Prioridad: ${this.prioridad}</p>
      <p>Asignada a: ${this.asignadoA}</p>
      ${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
    </article>
  `;
}

Al ejecutar elemento.asignadoA = 'Ana' desde la consola, Lit detecta el cambio a través del setter instalado sobre asignadoA (como se explicó en el apartado 7) y programa una actualización; tras esa actualización, el párrafo mostrará "Asignada a: Ana".

static properties = {
  // ...propiedades anteriores...
  completadaEn: { type: String, attribute: 'completada-en' },
};
<task-card titulo="Revisar el PR" completada-en="2026-07-01"></task-card>
  1. this.tareas.push(nuevaTarea) modifica el array ya existente al que apunta this.tareas, pero no ejecuta ninguna asignación sobre la propiedad tareas en sí; como el setter instalado por Lit (visto en el apartado 7) solo se dispara cuando hay una asignación real (this.tareas = ...), push pasa completamente inadvertido para Lit y no se programa ninguna actualización. Para que el cambio sea detectado, habría que reasignar la propiedad completa con un array nuevo, por ejemplo this.tareas = [...this.tareas, nuevaTarea];, de forma que el setter sí se ejecute.

Conclusión

En esta lección has aprendido a declarar propiedades reactivas de verdad con static properties, entendiendo el papel de sus opciones de configuración (type, attribute, reflect) y la sintaxis equivalente con el decorador @property. Has convertido <task-card> para que titulo, estado, prioridad y urgente sean propiedades reactivas reales, capaces de recibir valores tanto desde atributos HTML como desde JavaScript, y has entendido, a nivel de getters y setters, por qué eso las hace disparar actualizaciones automáticas donde antes no ocurría nada.

Sin embargo, no todas las propiedades de un componente deberían ser públicas de esta forma. <task-card> necesitará, en la siguiente lección, un dato interno —si la tarjeta está expandida o no— que no tiene sentido exponer como parte de su API pública ni como atributo HTML. Ese es el contenido de la siguiente lección, "Estado Interno con @state", donde conocerás la diferencia entre una propiedad pública y un estado privado del componente.

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