A lo largo de este módulo se ha usado, sin detenerse demasiado en ello, una distinción que conviene fijar con precisión antes de cerrar el tema: la diferencia entre un atributo HTML y una propiedad JavaScript. Esta lección explica esa diferencia en detalle, aclara en qué dirección sincroniza Lit por defecto y cuándo interesa activar la sincronización en el sentido contrario con reflect: true, y cierra el módulo aplicando todo lo aprendido para convertir tareas, en <task-list>, en una propiedad reactiva de tipo Array. Con ese último paso, cada <task-card> del tablero mostrará por fin los datos de su propia tarea, resolviendo la limitación que ha acompañado a TaskFlow desde el módulo 2.

Contenido

  1. Atributo y propiedad: dos cosas distintas que a menudo se confunden
  2. La sincronización por defecto: de atributo a propiedad
  3. reflect: true: sincronizar también en sentido contrario
  4. Cuándo interesa reflect: true (y su conexión con el módulo 4)
  5. Convirtiendo tareas en una propiedad reactiva de <task-list>
  6. Pasando datos propios a cada <task-card>: TaskFlow, por fin, dinámico
  7. Cierre del módulo: hacia los estilos

  1. Atributo y propiedad: dos cosas distintas que a menudo se confunden

Es habitual, sobre todo viniendo de HTML estático, dar por hecho que "atributo" y "propiedad" son la misma cosa. En la plataforma web, sin embargo, son dos conceptos relacionados pero distintos, y la distinción es clave para entender el sistema de reactividad de Lit con precisión.

  • Un atributo es una pieza de texto que aparece en el marcado HTML, entre las etiquetas de apertura de un elemento: <task-card titulo="Revisar el PR">. Como se explicó en la lección anterior, un atributo siempre es una cadena de texto, sin excepción, porque así lo define el estándar HTML.
  • Una propiedad es un campo del objeto JavaScript que representa ese elemento en el DOM: elemento.titulo. Una propiedad puede tener cualquier tipo de dato de JavaScript: texto, número, booleano, array, objeto, incluso una función.

Esta distinción no es exclusiva de Lit ni de los Custom Elements: existe en cualquier elemento nativo del navegador. Un <input> tiene un atributo value (texto) y una propiedad value (también texto, en este caso concreto, pero por decisión del propio elemento); un <input type="checkbox"> tiene un atributo checked (cuya sola presencia indica "marcado", como se vio con Boolean en la lección anterior) y una propiedad checked que es un booleano real de JavaScript. Lit no inventa este mecanismo: lo adopta y lo generaliza para las propiedades reactivas que se declaran en static properties.

Lo que hace static properties es, precisamente, decirle a Lit: "quiero que estas dos cosas —el atributo titulo y la propiedad titulo— estén conectadas automáticamente, y así es como debes convertir entre una y otra".

  1. La sincronización por defecto: de atributo a propiedad

Por defecto, cuando se declara una propiedad reactiva con type (o con state: true, aunque en ese caso, como se vio en la lección anterior, no hay atributo alguno con el que sincronizar), la sincronización ocurre en un único sentido: de atributo hacia propiedad.

static properties = {
  titulo: { type: String },
};

Con esta declaración, si el atributo titulo cambia en el HTML —ya sea porque el elemento se crea con ese atributo, o porque después se llama a elemento.setAttribute('titulo', 'Nuevo valor')—, Lit detecta ese cambio y actualiza automáticamente this.titulo con el valor convertido. Esta dirección es la que se ha usado, de hecho, en todos los ejemplos de HTML de las lecciones anteriores de este módulo (<task-card titulo="...">).

Pero el camino inverso no ocurre por defecto: si desde JavaScript se asigna elemento.titulo = 'Otro valor', la propiedad cambia (y dispara la actualización de render(), como se ha visto en todo el módulo), pero el atributo titulo en el HTML no se actualiza para reflejar ese nuevo valor. Si en ese momento se inspeccionara el elemento con las herramientas de desarrollo del navegador, el atributo seguiría mostrando el valor original, aunque la propiedad interna y lo que se ve en pantalla ya hubieran cambiado.

const tarjeta = document.querySelector('task-card');
// El HTML original era: <task-card titulo="Preparar la demo"></task-card>

tarjeta.titulo = 'Revisar el PR';
// this.titulo ahora vale 'Revisar el PR', y render() se actualiza.
// Pero el atributo HTML sigue siendo titulo="Preparar la demo".

  1. reflect: true: sincronizar también en sentido contrario

Cuando interesa que ese camino inverso también funcione —que un cambio en la propiedad, hecho desde JavaScript, se refleje de vuelta en el atributo HTML—, se activa con la opción reflect: true, ya mencionada de pasada en la primera lección del módulo:

static properties = {
  estado: { type: String, reflect: true },
};

Con reflect: true, cada vez que this.estado cambia (por cualquier vía: asignación directa, interpolación con punto en una plantilla padre, etc.), Lit actualiza automáticamente el atributo estado del elemento en el DOM real para que coincida con el nuevo valor de la propiedad, usando internamente una conversión equivalente a toAttribute (la misma función que se vio en el conversor personalizado de la lección anterior, aunque para los tipos soportados de serie Lit ya trae su propia lógica de conversión sin necesidad de escribirla a mano).

const tarjeta = document.querySelector('task-card');
tarjeta.estado = 'hecha';
// Con reflect: true, el atributo del DOM pasa a ser:
// <task-card estado="hecha">

Es importante remarcar que reflect: true añade trabajo adicional en cada actualización (Lit tiene que llamar a setAttribute sobre el elemento real del DOM, además de actualizar el campo interno de la propiedad), así que no conviene activarlo "por si acaso" en todas las propiedades; se reserva para los casos concretos donde de verdad aporta valor, que se ven en el siguiente apartado.

  1. Cuándo interesa reflect: true (y su conexión con el módulo 4)

Hay dos motivos principales, en la práctica, por los que interesa activar reflect: true en una propiedad:

  • Poder seleccionar el elemento por su estado desde fuera, por ejemplo con document.querySelector('task-card[estado="hecha"]'), algo ocasionalmente útil para pruebas automatizadas o para herramientas externas que inspeccionan el DOM sin tener acceso directo a las propiedades JavaScript.
  • Poder aplicar estilos CSS distintos según el valor de la propiedad, usando un selector de atributo, algo mucho más habitual en la práctica: si el atributo estado se refleja siempre en el DOM, se puede escribir en la hoja de estilos del propio componente una regla como :host([estado="hecha"]) { opacity: 0.6; }, que aplica un estilo distinto según el estado actual de la tarjeta, sin necesidad de calcular clases CSS dinámicamente en la plantilla.

Este segundo motivo es, con diferencia, el más relevante para TaskFlow, y es exactamente el puente hacia el siguiente módulo del curso: en el módulo 4, "Estilos en Componentes Lit", se explicará con detalle cómo escribir selectores como :host([atributo]) y por qué reflejar una propiedad como estado (o urgente) resulta muy útil para poder dar estilo visual distinto a una tarjeta urgente o a una tarjeta ya completada, sin necesidad de gestionar clases CSS a mano desde render(). Por ahora, en este módulo, basta con saber que reflect: true existe, qué hace exactamente, y que su utilidad principal aparecerá en el próximo módulo.

  1. Convirtiendo tareas en una propiedad reactiva de <task-list>

Con toda la teoría de propiedades reactivas ya cubierta en este módulo, es momento de cerrar la limitación que ha acompañado a TaskFlow desde la lección "Renderizado de Listas" del módulo 2: <task-list> recorre un array this.tareas, pero ese array es un campo de instancia simple, no reactivo, y cada <task-card> que genera se ve idéntica porque no recibe ningún dato propio.

Retoma src/components/task-list.js y declara tareas como propiedad reactiva de tipo Array, siguiendo exactamente el mismo patrón que se ha usado con <task-card> a lo largo de todo el módulo:

import { LitElement, html } from 'lit';
import './task-card.js';

class TaskList extends LitElement {
  static properties = {
    tareas: { type: Array },
  };

  constructor() {
    super();
    this.tareas = [
      { id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
      { id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
      { id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
    ];
  }

  render() {
    return html`
      <section>
        <h2>Mis tareas</h2>
        <div class="lista">
          ${this.tareas.map(
            (tarea) => html`<task-card></task-card>`
          )}
        </div>
      </section>
    `;
  }
}

customElements.define('task-list', TaskList);

De momento, este cambio por sí solo ya aporta algo: como tareas es ahora una propiedad reactiva de verdad, se le podría asignar un array completamente nuevo desde fuera (listaElemento.tareas = [...]) y <task-list> se actualizaría sola, regenerando toda la lista de tarjetas. Pero, como se señaló explícitamente al construir <task-list> en el módulo 2, sigue faltando la pieza final: pasar los datos de cada tarea a su <task-card> correspondiente.

  1. Pasando datos propios a cada <task-card>: TaskFlow, por fin, dinámico

Con <task-card> ya dotada de propiedades reactivas reales (titulo, estado, prioridad, urgente) desde la primera lección de este módulo, ya es posible completar render() en <task-list> usando la sintaxis de propiedades con punto que se presentó en la lección de expresiones e interpolación del módulo 2:

render() {
  return html`
    <section>
      <h2>Mis tareas</h2>
      <div class="lista">
        ${this.tareas.map(
          (tarea) => html`
            <task-card
              .titulo="${tarea.titulo}"
              .estado="${tarea.estado}"
              .prioridad="${tarea.prioridad}"
              .urgente="${tarea.urgente}"
            ></task-card>
          `
        )}
      </div>
    </section>
  `;
}

Analicemos qué ocurre ahora, paso a paso, cada vez que <task-list> renderiza. this.tareas.map(...) recorre el array de tareas, tal como se explicó en la lección "Renderizado de Listas"; por cada tarea, genera una plantilla <task-card> en la que cuatro propiedades se establecen mediante la sintaxis de punto (.titulo, .estado, .prioridad, .urgente), cada una interpolando el campo correspondiente del objeto tarea actual del recorrido. Como esas cuatro propiedades están declaradas como reactivas en <task-card> (desde la primera lección de este módulo), asignarlas mediante la sintaxis de punto las establece como propiedades JavaScript reales de cada instancia, no como atributos de texto, permitiendo pasar directamente el número (prioridad) y el booleano (urgente) sin ninguna conversión manual.

El resultado, al recargar la página, es el que se ha estado esperando desde el módulo 2: <task-list> muestra tres tarjetas, cada una con su propio título, su propia insignia de estado (calculada por renderInsigniaEstado() a partir de la propiedad estado recibida), su propia prioridad y, si corresponde, su propio aviso de urgencia. Cada <task-card> es ahora un componente genuinamente reutilizable: la misma clase TaskCard, parametrizada de forma distinta por cada instancia según los datos que recibe desde su padre.

Este patrón —un componente padre (<task-list>) que recorre una colección y pasa un fragmento de datos a cada componente hijo (<task-card>) mediante propiedades— es la forma estándar en la que los componentes se comunican de padre a hijo en Lit, y en la práctica totalidad de las bibliotecas modernas de interfaces. La comunicación en el sentido contrario —que <task-card> avise a <task-list> de que el usuario ha marcado una tarea como completada, por ejemplo— requiere un mecanismo distinto, basado en eventos personalizados, que es exactamente el contenido del módulo 5, "Eventos y Comunicación entre Componentes". Por ahora, con la comunicación padre-a-hijo ya resuelta, TaskFlow tiene ya un tablero de tareas plenamente dinámico en cuanto a mostrar datos, aunque todavía sin ninguna forma de que el usuario interactúe con ellos más allá del clic de expandir visto en la lección anterior.

  1. Cierre del módulo: hacia los estilos

Con esta lección se completa el módulo 3, "Propiedades y Estado Reactivo". El recorrido ha ido de menos a más: primero, declarar propiedades reactivas reales con static properties y entender qué las hace disparar actualizaciones automáticas; después, distinguir entre propiedades públicas y estado interno privado con state: true; a continuación, el catálogo completo de tipos soportados y cómo escribir un conversor personalizado para tipos que no encajan en ese catálogo; y, en esta última lección, la relación exacta entre atributos y propiedades, junto con la opción reflect para sincronizar en ambos sentidos.

TaskFlow, como resultado de todo el módulo, ha dado un salto cualitativo: <task-card> tiene ahora propiedades reactivas completas (titulo, estado, prioridad, urgente, fechaLimite), un estado interno propio (expandida), y <task-list> recibe su colección de tareas como una propiedad reactiva de tipo Array y pasa a cada tarjeta hija los datos que le corresponden. El tablero, por primera vez en el curso, muestra datos realmente distintos en cada tarjeta y se actualiza solo cuando esos datos cambian.

Sin embargo, si has estado siguiendo el curso ejecutando los ejemplos en el navegador, habrás notado que, pese a toda esta reactividad de datos, el aspecto visual de <task-card> y <task-list> sigue siendo el del HTML sin ningún estilo: sin colores, sin espaciados cuidados, sin ninguna diferenciación visual clara entre una tarjeta urgente y una que no lo es. Los datos ya son dinámicos; falta que se vean bien. Esa es, precisamente, la tarea del módulo 4, "Estilos en Componentes Lit", donde aprenderás a usar static styles y la función css para dar a los componentes de TaskFlow una apariencia encapsulada y coherente, y donde retomarás la reflexión de atributos vista en esta lección para poder aplicar estilos distintos según el estado o la urgencia de cada tarjeta mediante selectores como :host([estado="hecha"]).

Errores Comunes y Consejos

  • Esperar que cambiar una propiedad desde JavaScript actualice el atributo sin reflect: true: como se explicó en el apartado 2, la sincronización por defecto va solo de atributo a propiedad; si se necesita el camino inverso, hay que activarlo explícitamente con reflect: true, y solo en las propiedades donde de verdad aporte valor.
  • Activar reflect: true en todas las propiedades sin necesidad: como se apuntó en el apartado 3, reflejar añade una llamada a setAttribute en cada actualización; en propiedades que nunca se van a usar como selector CSS ni se van a inspeccionar desde fuera, esa sincronización adicional es trabajo desperdiciado.
  • Confundir la sintaxis de punto (.propiedad) con un atributo normal: como se recordó en el apartado 6, .titulo="${tarea.titulo}" asigna directamente la propiedad JavaScript, sin pasar por texto ni por ningún conversor de atributo; es la forma recomendada de pasar datos de tipos no textuales (números, booleanos, arrays, objetos) de un componente padre a un componente hijo.
  • Pasar un array completo de tareas mutando el array existente en lugar de reasignarlo: si más adelante se añade una tarea a <task-list> con this.tareas.push(nuevaTarea) en lugar de this.tareas = [...this.tareas, nuevaTarea], la propiedad reactiva tareas no detectará el cambio, exactamente por el motivo explicado en la primera lección de este módulo sobre mutación frente a reasignación.

Ejercicios

  1. Añade una cuarta tarea al array inicial de <task-list>, con sus propios titulo, estado, prioridad y urgente, y comprueba que la cuarta <task-card> generada muestra correctamente sus propios datos, distintos de las otras tres.
  2. Declara en <task-card> la propiedad estado con reflect: true (además de type: String, que ya tenía) y comprueba, inspeccionando el elemento con las herramientas de desarrollo del navegador tras asignar elemento.estado = 'hecha' desde la consola, que el atributo estado del DOM cambia también a "hecha".
  3. Explica con tus propias palabras, apoyándote en el apartado 4, por qué reflejar la propiedad estado (con reflect: true) tiene más sentido práctico en TaskFlow que reflejar, por ejemplo, la propiedad fechaLimite de tipo fecha.

Soluciones

this.tareas = [
  { id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
  { id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
  { id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
  { id: 4, titulo: 'Actualizar la documentación', estado: 'pendiente', prioridad: 1, urgente: false },
];

Al recargar la página, Array.map genera una cuarta plantilla <task-card> con sus propias propiedades .titulo, .estado, .prioridad y .urgente interpoladas a partir de este nuevo objeto, mostrando datos distintos de las tres tarjetas anteriores, sin tocar el resto de render().

static properties = {
  titulo: { type: String },
  estado: { type: String, reflect: true },
  prioridad: { type: Number },
  urgente: { type: Boolean },
  expandida: { state: true },
  fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
};

Tras ejecutar elemento.estado = 'hecha' en la consola, al inspeccionar el elemento en las herramientas de desarrollo se observa que la etiqueta pasa a mostrar <task-card estado="hecha" ...>, porque reflect: true hace que Lit llame automáticamente a setAttribute('estado', 'hecha') sobre el elemento cada vez que la propiedad cambia.

  1. Reflejar estado tiene sentido práctico porque, como se explica en el apartado 4, permite escribir selectores CSS basados en atributo (:host([estado="hecha"])) para dar un estilo visual distinto a las tarjetas según su estado, algo que se desarrollará en el módulo 4, y también permite localizar tarjetas por su estado desde fuera con selectores como task-card[estado="hecha"]. fechaLimite, en cambio, es un valor casi continuo (una fecha concreta, potencialmente distinta en cada tarea) que rara vez tiene sentido usar como selector CSS o como filtro por igualdad exacta de atributo; además, su conversor personalizado ya transforma la fecha a texto de una forma pensada para lectura por parte de Lit, no necesariamente para un uso cómodo como selector, por lo que reflejarla aportaría poco valor práctico frente al coste adicional de sincronizarla en cada actualización.

Conclusión

En esta última lección del módulo 3 has terminado de fijar la distinción entre atributo (texto, en el HTML) y propiedad (cualquier tipo, en JavaScript), entendiendo que Lit sincroniza por defecto solo de atributo a propiedad, y que reflect: true activa también el camino inverso para los casos en los que interesa —típicamente, para poder aplicar estilos CSS según el valor de una propiedad, como se explotará en el módulo 4. Con esa base, has convertido tareas, en <task-list>, en una propiedad reactiva de tipo Array, y has completado la plantilla de <task-list> para pasar los datos de cada tarea a su <task-card> correspondiente mediante la sintaxis de propiedades con punto, consiguiendo que cada tarjeta del tablero muestre por fin la información de su propia tarea.

Con esto se cierra el módulo "Propiedades y Estado Reactivo": <task-card> y <task-list> ya son componentes reactivos de verdad, con datos propios, estado interno y una comunicación de padre a hijo bien resuelta. Ahora que los datos son reactivos y cada tarjeta muestra la información correcta, toca ocuparse de cómo se ven: en el módulo 4, "Estilos en Componentes Lit", aprenderás a dar estilo visual a los componentes de TaskFlow.

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