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

  1. Listas de plantillas: un array es un valor interpolable
  2. Array.map como técnica principal
  3. El problema de la identidad al actualizar listas
  4. La directiva repeat y el concepto de key: una mención de paso
  5. Cuándo basta con map y cuándo conviene repeat
  6. Construyendo <task-list>

  1. 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.

render() {
  return html`
    <ul>
      ${['Comprar pan', 'Revisar el PR', 'Llamar al cliente']}
    </ul>
  `;
}

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.

  1. Array.map como técnica principal

El 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\

  • ${tarea}
  • `). El método maprecorre cada elemento del arraythis.tareasy, por cada uno, ejecuta la función indicada, que en este caso devuelve una plantillahtmlcon un
  • conteniendo 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>
      `;
    }

    1. 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.

    1. La directiva repeat y el concepto de key: una mención de paso

    Para 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.

    1. Cuándo basta con map y cuándo conviene repeat

    No 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.

    1. Construyendo <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` trae en su propio constructor.

    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:` ya sabe recorrer un array y generar una tarjeta por elemento, que es la parte de este módulo, pero conectar cada tarjeta con sus propios datos es la parte del módulo 3.

    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 import del componente hijo: si <task-list> no importa task-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 map como si fuera una clave estable: es tentador escribir this.tareas.map((tarea, indice) => ...) y pensar en indice como 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 usar repeat más adelante en el curso), hay que usar un identificador propio de los datos, como tarea.id.
    • Renderizar listas enormes sin ninguna estrategia de paginado o virtualización: Array.map funciona 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

    1. Añade una cuarta tarea al array this.tareas de <task-list> y comprueba que aparece una cuarta tarjeta al recargar la página.
    2. Modifica render() de <task-list> para que, antes de la lista de tarjetas, muestre un párrafo con el número total de tareas, usando this.tareas.length interpolado directamente (sin necesidad de map, ya que es un único valor).
    3. 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 usara repeat con tarea.id como clave en lugar de Array.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>
      `;
    }
    1. 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. Con repeat y tarea.id como 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 (mismo id) 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

    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