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
- Qué significa exactamente que una propiedad sea "reactiva"
- Declarar propiedades con
static properties - Las opciones de configuración:
type,attribute,reflect - La alternativa con el decorador
@property - Convirtiendo
<task-card>a propiedades reactivas reales - Usar
<task-card>desde HTML (atributos) y desde JavaScript (propiedades) - Qué pasa por dentro: el mecanismo de los accessors
- 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.
- Declarar propiedades con
static properties
static propertiesLa 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 propertiesse 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 propertiesno asigna ningún valor por sí sola: solo declara la existencia de la propiedad y su configuración. Es responsabilidad delconstructor(tras la llamada obligatoria asuper()) 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.
- Las opciones de configuración:
type, attribute, reflect
type, attribute, reflectEl 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.
- La alternativa con el decorador
@property
@propertyComo 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.
- Convirtiendo
<task-card> a propiedades reactivas reales
<task-card> a propiedades reactivas realesCon 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.
- Usar
<task-card> desde HTML (atributos) y desde JavaScript (propiedades)
<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):
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:
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.
- 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 propertiesy esperar que sea reactiva: si se asignathis.algo = valorsin haber declaradoalgoenstatic 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 propertiesen lugar de en elconstructor:static propertiessolo 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 elconstructor, trassuper(), 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 propiedadprioridadAltasin configuración explícita deattributegenera por defecto un atributoprioridadalta(todo junto), noprioridad-alta. Si se quiere un atributo enkebab-case, hay que indicarlo explícitamente.
Ejercicios
- Añade a
<task-card>una nueva propiedad reactiva llamadaasignadoA, de tipoString, 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, queelemento.asignadoA = 'Ana'actualiza la pantalla. - Declara en
<task-card>una propiedadcompletadaEn, de tipoString, cuyo atributo HTML se llame explícitamentecompletada-en(usando la opciónattributevista en el apartado 3). Escribe el HTML de ejemplo que usarías para establecer esa propiedad desde una etiqueta<task-card>. - Basándote en el apartado 7, explica con tus propias palabras por qué
this.tareas.push(nuevaTarea)sobre una propiedad reactivatareasde tipoArrayno 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' },
};this.tareas.push(nuevaTarea)modifica el array ya existente al que apuntathis.tareas, pero no ejecuta ninguna asignación sobre la propiedadtareasen sí; como el setter instalado por Lit (visto en el apartado 7) solo se dispara cuando hay una asignación real (this.tareas = ...),pushpasa 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 ejemplothis.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
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
