TaskFlow no tiene mucho sentido si solo puede mostrar una tarea a la vez. Un tablero Kanban real necesita mostrar colecciones enteras de tareas: todas las pendientes, todas las que están en progreso, todas las de una persona concreta. Esta lección explica cómo recorrer un array de datos dentro de una plantilla Lit usando Array.map, qué problemas de identidad pueden aparecer al renderizar listas que cambian con el tiempo, y por qué en esos casos conviene una directiva específica que se detallará más adelante en el curso. Con esta técnica construirás <task-list>, el primer componente de TaskFlow capaz de mostrar varias tareas a partir de una colección de datos.
Contenido
- Listas de plantillas: un array es un valor interpolable
Array.mapcomo técnica principal- El problema de la identidad al actualizar listas
- La directiva
repeaty el concepto dekey: una mención de paso - Cuándo basta con
mapy cuándo convienerepeat - Construyendo
<task-list>
- Listas de plantillas: un array es un valor interpolable
En la lección "El Motor de Plantillas de Lit" se mencionó, de pasada, que un array de valores es un contenido válido para interpolar dentro de una plantilla html. Es momento de detenerse en esa idea: cuando el resultado de una interpolación es un array, Lit no lo convierte a texto (no aparecerá algo como "tarea1,tarea2,tarea3" en pantalla); en su lugar, renderiza cada elemento del array como si fuera una interpolación independiente, colocando cada uno a continuación del anterior.
Este ejemplo, aunque poco útil tal cual, ya demuestra el comportamiento: los tres textos del array aparecerían uno tras otro dentro del <ul>, sin ninguna etiqueta <li> que los envuelva individualmente, porque el array contiene solo texto plano. El caso realmente útil aparece cuando, en lugar de un array de textos sueltos, se interpola un array de plantillas html, una por cada elemento de los datos originales. Ahí es donde entra Array.map.
Array.map como técnica principal
Array.map como técnica principalEl patrón estándar para renderizar una lista en Lit consiste en usar el método map de los arrays, ya disponible en JavaScript estándar sin ninguna dependencia de Lit, para transformar un array de datos en un array de plantillas html:
import { LitElement, html } from 'lit';
class ListaDeTareas extends LitElement {
constructor() {
super();
this.tareas = ['Comprar pan', 'Revisar el PR', 'Llamar al cliente'];
}
render() {
return html`
<ul>
${this.tareas.map((tarea) => html`<li>${tarea}</li>`)}
</ul>
`;
}
}Analicemos la expresión clave: this.tareas.map((tarea) => html\
. El método maprecorre cada elemento del arraythis.tareasy, por cada uno, ejecuta la función indicada, que en este caso devuelve una plantillahtmlcon unconteniendo ese texto. El resultado demapno es ya el array de textos original, sino un **nuevo array de plantillashtml**, uno por cada tarea. Ese nuevo array es exactamente el tipo de valor descrito en el apartado 1: un array cuyo contenido son plantillas, así que Lit lo renderiza colocando cada uno tras otro dentro del- `.
Este patrón es tan habitual en Lit (y, en general, en cualquier framework moderno de interfaces basado en JavaScript) que conviene memorizarlo como una construcción de una sola línea: array.map((elemento) => html\...`)`. Se puede usar en cualquier punto de una plantilla donde tenga sentido repetir una estructura por cada elemento de una colección: filas de una tabla, opciones de un desplegable, tarjetas de un tablero, elementos de un menú.
Si los objetos de la colección son más complejos que simples cadenas de texto, el patrón funciona exactamente igual, accediendo a las propiedades de cada objeto dentro de la función:
render() {
return html`
<ul>
${this.tareas.map((tarea) => html`
<li>${tarea.titulo} — ${tarea.estado}</li>
`)}
</ul>
`;
}
- El problema de la identidad al actualizar listas
Mientras una lista se renderiza una única vez y nunca vuelve a cambiar, map no presenta ningún problema. Los inconvenientes aparecen cuando la lista cambia con el tiempo: se añaden tareas, se eliminan, o se reordenan (por ejemplo, al arrastrar una tarjeta de una columna a otra en un tablero Kanban, algo que TaskFlow hará en módulos posteriores).
Cuando Lit vuelve a ejecutar render() con un array distinto (aunque solo haya cambiado ligeramente respecto al anterior), y ese array se procesa con map como en el apartado 2, Lit compara, por defecto, la posición de cada plantilla resultante con la posición que ocupaba en el renderizado anterior, no el contenido lógico de cada elemento. Es decir: "la plantilla que estaba en la posición 3 la última vez" se compara con "la plantilla que está en la posición 3 esta vez", sin tener en cuenta si se trata realmente de la misma tarea original o de una completamente distinta que simplemente ha caído en esa posición tras reordenar el array.
En la práctica, esto puede producir dos tipos de problemas:
- Ineficiencia: si se inserta una tarea nueva al principio de una lista de cien elementos, todas las posiciones se desplazan una hacia abajo. Comparando por posición, Lit puede terminar actualizando el contenido de las cien tarjetas (porque el "elemento en la posición N" ha cambiado en las cien posiciones), en lugar de reconocer que noventa y nueve tarjetas son exactamente las mismas de antes y solo hay que insertar una nueva al principio.
- Pérdida de estado del DOM: si algún nodo dentro de una de esas plantillas tenía estado propio del navegador (por ejemplo, el foco de un campo de texto, una animación en marcha, el texto seleccionado por el usuario), reordenar por posición puede hacer que ese estado "salte" a un elemento lógico distinto del que lo tenía originalmente, porque Lit, al no saber qué elemento es "el mismo de antes", puede reutilizar el nodo físico de una posición para representar ahora datos distintos.
- La directiva
repeat y el concepto de key: una mención de paso
repeat y el concepto de key: una mención de pasoPara resolver este problema, Lit ofrece una directiva llamada repeat, pensada específicamente para listas que cambian con el tiempo (se insertan, eliminan o reordenan elementos). Su forma básica es la siguiente:
import { repeat } from 'lit/directives/repeat.js';
render() {
return html`
<ul>
${repeat(
this.tareas,
(tarea) => tarea.id,
(tarea) => html`<li>${tarea.titulo}</li>`
)}
</ul>
`;
}repeat recibe tres argumentos: el array de datos, una función que calcula una clave única (key) para cada elemento —normalmente un identificador estable, como tarea.id—, y una función que genera la plantilla para cada elemento, igual que se haría con map. Gracias a esa clave, Lit puede identificar cada elemento por su identidad lógica real, no por su posición en el array: si una tarea con id: 'abc' se mueve de la posición 3 a la posición 0, repeat reconoce que sigue siendo la misma tarea y mueve el nodo DOM existente, en lugar de destruirlo y recrear uno nuevo con contenido distinto.
Esta directiva, junto con el resto del catálogo de directivas incorporadas en Lit, se estudia en profundidad en el módulo 7, "Directivas y Funcionalidades Avanzadas de Plantillas". Se menciona aquí, de forma anticipada, únicamente para que sepas que existe una solución concreta al problema descrito en el apartado 3, y para que reconozcas la sintaxis si la encuentras antes de llegar a ese módulo.
- Cuándo basta con
map y cuándo conviene repeat
map y cuándo conviene repeatNo toda lista necesita repeat; usarlo "por si acaso" en cualquier lista añade una dependencia y una capa de complejidad que a veces no aporta nada. Una guía práctica para decidir:
| Situación | Técnica recomendada |
|---|---|
| La lista se renderiza una vez y no vuelve a cambiar en la vida del componente | Array.map es suficiente |
| La lista cambia, pero siempre se reconstruye entera desde cero (por ejemplo, se sustituye completamente el array en cada actualización, sin conservar elementos) | Array.map sigue siendo razonable |
| La lista sufre inserciones, eliminaciones o reordenaciones frecuentes, y conservar el estado del DOM de cada elemento importa (foco, animaciones, entradas de formulario) | repeat, con una clave estable como id |
| Los elementos de la lista son complejos o costosos de volver a renderizar, y se quiere evitar recalcular los que no han cambiado de posición lógica | repeat |
En TaskFlow, por ejemplo, cuando más adelante en el curso se implemente arrastrar tarjetas entre columnas del tablero Kanban, ese será exactamente el escenario en el que repeat aportará una diferencia notable: las tarjetas se moverán de posición sin perder su identidad ni su estado interno. Por ahora, con datos de ejemplo estáticos que no cambian durante la ejecución del componente, Array.map es la herramienta adecuada y la que se usará durante el resto de este módulo.
- Construyendo
<task-list>
<task-list>Con la técnica de Array.map ya explicada, es el momento de construir el segundo componente real de TaskFlow: <task-list>. Su responsabilidad es sencilla: recibir una colección de tareas y renderizar un <task-card> por cada una. Igual que en las lecciones anteriores, la colección se declarará como un campo de instancia simple, no como una propiedad reactiva (eso llega en el módulo 3).
Primero, asegúrate de que task-card.js exporta o al menos registra su elemento como en lecciones anteriores (se asume aquí la versión de la lección "Renderizado Condicional", con insignia de estado). Después, crea src/components/task-list.js:
import { LitElement, html } from 'lit';
import './task-card.js';
class TaskList extends LitElement {
constructor() {
super();
// Array de instancia simple, todavía sin reactividad real (módulo 3).
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso' },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente' },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha' },
];
}
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);Hay un detalle importante que conviene señalar explícitamente en este punto del curso: en la línea html\dentro delmap, se está creando una instancia de por cada tarea del array, pero **todavía no se le está pasando ningún dato concreto de esa tarea**. Comosigue sin tener propiedades reactivas reales, no existe todavía una forma limpia de decirle "muestra el título de *esta* tarea en concreto"; por eso, en este punto del curso, las tres tarjetas mostradas porse verán todas idénticas, con los valores fijos que
Esta limitación, precisamente, es el argumento más claro para justificar por qué el módulo 3 es imprescindible: en cuanto <task-card> declare propiedades reactivas reales, se podrá escribir algo como html\<task-card .titulo="${tarea.titulo}" .estado="${tarea.estado}">`(usando la sintaxis de propiedades con punto vista en la lección anterior), y cada tarjeta mostrará entonces los datos de su propia tarea. Es importante que quede clara esta limitación ahora, en lugar de dejarla como una sorpresa confusa:
Para usar el nuevo componente, actualiza index.html:
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>TaskFlow</title> <script type="module" src="/src/components/task-list.js"></script> </head> <body> <h1>TaskFlow</h1> <task-list></task-list> </body> </html>
Fíjate en que index.html ya no necesita importar task-card.js directamente: basta con que task-list.js lo importe (import './task-card.js';), porque ese import registra igualmente el elemento <task-card> en el navegador, y <task-list> lo usa internamente en su propia plantilla. Al recargar la página, deberías ver el título "Mis tareas" seguido de tres tarjetas, una por cada elemento del array this.tareas, aunque las tres muestren, de momento, el mismo contenido de ejemplo.
Errores Comunes y Consejos
- Olvidar el
importdel componente hijo: si<task-list>no importatask-card.js(directamente o de forma indirecta, a través de otro fichero que sí lo haga), el navegador no sabrá qué hacer con la etiqueta<task-card>y la tratará como un elemento desconocido, sin ningún error visible más allá de que no se vea nada dentro. - Esperar que cada
<task-card>muestre datos distintos ya en este módulo: como se ha explicado en el apartado 6, sin propiedades reactivas todavía no existe una forma limpia de pasar datos distintos a cada instancia; las tarjetas se verán iguales hasta el módulo 3, y esto es intencionado. - Usar el índice de
mapcomo si fuera una clave estable: es tentador escribirthis.tareas.map((tarea, indice) => ...)y pensar enindicecomo un identificador único de cada tarea. No lo es: el índice depende de la posición actual en el array, que cambia si se reordena o se inserta un elemento al principio. Para identificar de forma estable un elemento (por ejemplo, al usarrepeatmás adelante en el curso), hay que usar un identificador propio de los datos, comotarea.id. - Renderizar listas enormes sin ninguna estrategia de paginado o virtualización:
Array.mapfunciona bien para listas de tamaño razonable (decenas o incluso unos pocos cientos de elementos), pero renderizar miles de elementos de golpe puede volverse lento independientemente de la biblioteca usada. Esta consideración de rendimiento a gran escala se retoma en el módulo 9, "Pruebas y Buenas Prácticas".
Ejercicios
- Añade una cuarta tarea al array
this.tareasde<task-list>y comprueba que aparece una cuarta tarjeta al recargar la página. - Modifica
render()de<task-list>para que, antes de la lista de tarjetas, muestre un párrafo con el número total de tareas, usandothis.tareas.lengthinterpolado directamente (sin necesidad demap, ya que es un único valor). - Investiga (consultando la documentación oficial de Lit sobre la directiva
repeat, o el apartado 4 de esta lección) qué ocurriría de distinto, en términos de qué nodos del DOM se reutilizan, si se usararepeatcontarea.idcomo clave en lugar deArray.map, al insertar una nueva tarea al principio del array. Redacta la respuesta con tus propias palabras, sin necesidad de programarlo todavía.
Soluciones
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso' },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente' },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha' },
{ id: 4, titulo: 'Actualizar la documentación', estado: 'pendiente' },
];Al recargar la página, Array.map genera automáticamente una cuarta plantilla <task-card>, sin necesidad de tocar el resto de render().
render() {
return html`
<section>
<h2>Mis tareas</h2>
<p>Total: ${this.tareas.length} tareas</p>
<div class="lista">
${this.tareas.map((tarea) => html`<task-card></task-card>`)}
</div>
</section>
`;
}- Con
Array.map, al insertar una tarea nueva al principio del array, todas las tareas existentes pasan a ocupar una posición distinta a la que tenían (la que era la posición 0 pasa a ser la 1, y así sucesivamente); como Lit compara por posición en este caso, es posible que reconstruya o actualice el contenido de todas las tarjetas existentes, no solo que inserte una nueva al principio. Conrepeatytarea.idcomo clave, Lit identifica cada tarea por su identificador, independientemente de la posición que ocupe en el array; al insertar una tarea nueva al principio, reconocería que las demás tareas son las mismas de antes (mismoid) y simplemente insertaría un nuevo nodo DOM al principio, sin tocar ni reconstruir los nodos ya existentes de las demás tarjetas.
Conclusión
En esta lección has aprendido a renderizar colecciones de datos dentro de una plantilla Lit combinando Array.map con interpolación de arrays de plantillas html, la técnica estándar para listas que no cambian de forma compleja durante la vida del componente. Has entendido también por qué las listas que se reordenan o modifican con frecuencia pueden sufrir problemas de identidad si se comparan solo por posición, y por qué existe la directiva repeat con clave (key) para esos casos, cuyo detalle se estudiará en el módulo 7. Con esta base, TaskFlow cuenta ya con <task-list>, capaz de recorrer un array de tareas de ejemplo y generar un <task-card> por cada una, aunque todavía sin poder pasarle datos distintos a cada tarjeta: esa pieza queda pendiente, de forma consciente, para el módulo 3.
En la última lección de este módulo, "El Ciclo de Renderizado", darás un paso atrás para entender algo que has estado dando por sentado hasta ahora: cómo y cuándo decide Lit exactamente cuándo volver a ejecutar render(), por qué ese proceso es asíncrono, y por qué nunca se debe modificar el DOM a mano dentro de ese método, cerrando así el módulo antes de dar el salto a las propiedades reactivas de verdad en el módulo 3.
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
