TaskFlow doesn't make much sense if it can only show one task at a time. A real Kanban board needs to display entire collections of tasks: all the pending ones, all the ones in progress, all the ones belonging to a specific person. This lesson explains how to iterate over an array of data inside a Lit template using Array.map, what identity problems can show up when rendering lists that change over time, and why in those cases a specific directive—covered in detail later in the course—is preferable. With this technique you'll build <task-list>, TaskFlow's first component capable of showing several tasks from a collection of data.

Contents

  1. Template lists: an array is an interpolatable value
  2. Array.map as the primary technique
  3. The identity problem when updating lists
  4. The repeat directive and the concept of key: a passing mention
  5. When map is enough and when repeat is preferable
  6. Building <task-list>

  1. Template lists: an array is an interpolatable value

In the lesson "Lit's Template Engine" it was mentioned, in passing, that an array of values is valid content to interpolate inside an html template. It's time to stop and look closer at that idea: when the result of an interpolation is an array, Lit doesn't convert it to text (you won't see something like "tarea1,tarea2,tarea3" on screen); instead, it renders each element of the array as if it were an independent interpolation, placing each one right after the previous one.

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

This example, although not very useful as written, already demonstrates the behavior: the three strings in the array would appear one after another inside the <ul>, with no <li> tag wrapping each one individually, because the array contains only plain text. The genuinely useful case appears when, instead of an array of plain strings, you interpolate an array of html templates, one per element of the original data. That's where Array.map comes in.

  1. Array.map as the primary technique

The standard pattern for rendering a list in Lit is to use the array method map, already available in standard JavaScript with no dependency on Lit, to transform an array of data into an array of html templates:

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>
    `;
  }
}

Let's analyze the key expression: this.tareas.map((tarea) => html\

  • ${tarea}
  • `). The mapmethod walks through each element of thethis.tareasarray and, for each one, runs the given function, which in this case returns anhtmltemplate with an
  • containing that text. The result ofmapis no longer the original array of strings, but a **new array ofhtmltemplates**, one per task. That new array is exactly the kind of value described in section 1: an array whose content is templates, so Lit renders it by placing each
  • one after another inside the
      `.

      This pattern is so common in Lit (and, generally, in any modern JavaScript-based UI framework) that it's worth memorizing as a one-line construct: array.map((elemento) => html\...`)`. You can use it anywhere in a template where it makes sense to repeat a structure for each element of a collection: table rows, dropdown options, cards on a board, menu items.

      If the objects in the collection are more complex than plain strings, the pattern works exactly the same way, accessing each object's properties inside the function:

  • render() {
      return html`
        <ul>
          ${this.tareas.map((tarea) => html`
            <li>${tarea.titulo} — ${tarea.estado}</li>
          `)}
        </ul>
      `;
    }

    1. The identity problem when updating lists

    As long as a list renders once and never changes again, map presents no problems at all. The trouble shows up when the list changes over time: tasks are added, removed, or reordered (for example, by dragging a card from one column to another on a Kanban board, something TaskFlow will do in later modules).

    When Lit re-runs render() with a different array (even if it has only changed slightly from the previous one), and that array is processed with map as in section 2, Lit compares, by default, the position of each resulting template with the position it occupied in the previous render, not the logical content of each element. In other words: "the template that was at position 3 last time" gets compared against "the template that's at position 3 this time", regardless of whether it's really the same original task or a completely different one that simply landed on that position after the array was reordered.

    In practice, this can cause two kinds of problems:

    • Inefficiency: if a new task is inserted at the beginning of a list of a hundred elements, every position shifts down by one. Comparing by position, Lit may end up updating the content of all one hundred cards (because "the element at position N" changed at all one hundred positions), instead of recognizing that ninety-nine cards are exactly the same as before and only one new card needs to be inserted at the start.
    • Loss of DOM state: if some node inside one of those templates held browser-specific state (for example, the focus on a text field, an animation in progress, text selected by the user), reordering by position can make that state "jump" to a different logical element than the one that originally held it, because Lit, having no way to know which element is "the same one as before", may reuse the physical node from a position to now represent different data.

    1. The repeat directive and the concept of key: a passing mention

    To solve this problem, Lit offers a directive called repeat, designed specifically for lists that change over time (elements get inserted, removed, or reordered). Its basic form is as follows:

    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 takes three arguments: the array of data, a function that computes a unique key (key) for each element—typically a stable identifier, such as tarea.id—and a function that generates the template for each element, just like you would with map. Thanks to that key, Lit can identify each element by its real logical identity, not by its position in the array: if a task with id: 'abc' moves from position 3 to position 0, repeat recognizes that it's still the same task and moves the existing DOM node, instead of destroying it and recreating a new one with different content.

    This directive, along with the rest of Lit's catalog of built-in directives, is studied in depth in module 7, "Directives and Advanced Template Features". It's mentioned here ahead of time only so you know a concrete solution to the problem described in section 3 exists, and so you recognize the syntax if you come across it before reaching that module.

    1. When map is enough and when repeat is preferable

    Not every list needs repeat; using it "just in case" on every list adds a dependency and a layer of complexity that sometimes brings nothing to the table. A practical guide for deciding:

    Situation Recommended technique
    The list renders once and never changes again during the component's life Array.map is enough
    The list changes, but it's always rebuilt entirely from scratch (for example, the array is fully replaced on every update, without keeping any elements) Array.map remains reasonable
    The list undergoes frequent insertions, removals, or reorderings, and preserving each element's DOM state matters (focus, animations, form input) repeat, with a stable key such as id
    The list's elements are complex or expensive to re-render, and you want to avoid recomputing the ones that haven't changed logical position repeat

    In TaskFlow, for example, when card dragging between Kanban board columns gets implemented later in the course, that will be exactly the scenario where repeat makes a noticeable difference: cards will move position without losing their identity or their internal state. For now, with static sample data that doesn't change during the component's execution, Array.map is the right tool, and the one that will be used for the rest of this module.

    1. Building <task-list>

    With the Array.map technique already explained, it's time to build TaskFlow's second real component: <task-list>. Its responsibility is simple: receive a collection of tasks and render a <task-card> for each one. Just like in previous lessons, the collection will be declared as a plain instance field, not as a reactive property (that comes in module 3).

    First, make sure task-card.js exports or at least registers its element as in previous lessons (the version from the lesson "Conditional Rendering", with a status badge, is assumed here). Then, create src/components/task-list.js:

    import { LitElement, html } from 'lit';
    import './task-card.js';
    
    class TaskList extends LitElement {
      constructor() {
        super();
        // Plain instance array, still without real reactivity (module 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);

    There's an important detail worth pointing out explicitly at this point in the course: on the line html\`inside themap, an instance of is being created for each task in the array, but **no specific data from that task is being passed to it yet**. Sincestill has no real reactive properties, there's no clean way yet to tell it "show the title of *this particular* task"; that's why, at this point in the course, the three cards shown bywill all look identical, with the fixed values` carries in its own constructor.

    This limitation is, precisely, the clearest argument for why module 3 is essential: as soon as <task-card> declares real reactive properties, you'll be able to write something like html\<task-card .titulo="${tarea.titulo}" .estado="${tarea.estado}">`(using the dot-property syntax seen in the previous lesson), and each card will then show the data of its own task. It's important to make this limitation clear now, rather than leaving it as a confusing surprise:` already knows how to walk an array and generate a card per element, which is this module's part, but connecting each card with its own data is module 3's part.

    To use the new component, update 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>

    Notice that index.html no longer needs to import task-card.js directly: it's enough that task-list.js imports it (import './task-card.js';), because that import still registers the <task-card> element in the browser, and <task-list> uses it internally in its own template. When you reload the page, you should see the heading "Mis tareas" followed by three cards, one per element of the this.tareas array, even though all three still show, for now, the same sample content.

    Common Mistakes and Tips

    • Forgetting the child component's import: if <task-list> doesn't import task-card.js (directly, or indirectly through some other file that does), the browser won't know what to do with the <task-card> tag and will treat it as an unknown element, with no visible error beyond the fact that nothing shows up inside it.
    • Expecting each <task-card> to show different data already at this module: as explained in section 6, without reactive properties there's still no clean way to pass different data to each instance; the cards will look the same until module 3, and this is intentional.
    • Using the map index as if it were a stable key: it's tempting to write this.tareas.map((tarea, indice) => ...) and think of indice as a unique identifier for each task. It isn't: the index depends on the current position in the array, which changes if elements are reordered or a new one is inserted at the start. To stably identify an element (for example, when using repeat later in the course), you need to use an identifier that belongs to the data itself, such as tarea.id.
    • Rendering huge lists with no pagination or virtualization strategy: Array.map works fine for lists of a reasonable size (tens or even a few hundred elements), but rendering thousands of elements at once can become slow regardless of the library used. This large-scale performance consideration is revisited in module 9, "Testing and Best Practices".

    Exercises

    1. Add a fourth task to <task-list>'s this.tareas array and check that a fourth card appears when you reload the page.
    2. Modify <task-list>'s render() so that, before the list of cards, it shows a paragraph with the total number of tasks, using this.tareas.length interpolated directly (no need for map, since it's a single value).
    3. Investigate (by checking Lit's official documentation on the repeat directive, or section 4 of this lesson) what would happen differently, in terms of which DOM nodes get reused, if repeat were used with tarea.id as the key instead of Array.map, when inserting a new task at the start of the array. Write your answer in your own words, with no need to code it yet.

    Solutions

    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' },
    ];

    When you reload the page, Array.map automatically generates a fourth <task-card> template, with no need to touch the rest of 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. With Array.map, when a new task is inserted at the start of the array, every existing task ends up occupying a different position than before (what was position 0 becomes position 1, and so on); since Lit compares by position in this case, it may end up rebuilding or updating the content of every existing card, not just inserting a new one at the start. With repeat and tarea.id as the key, Lit identifies each task by its identifier, regardless of the position it occupies in the array; when inserting a new task at the start, it would recognize that the other tasks are the same ones as before (same id) and simply insert a new DOM node at the start, without touching or rebuilding the already-existing nodes of the other cards.

    Conclusion

    In this lesson you learned to render collections of data inside a Lit template by combining Array.map with interpolation of arrays of html templates, the standard technique for lists that don't change in complex ways during the component's life. You've also understood why lists that get reordered or modified frequently can suffer identity problems when compared only by position, and why the repeat directive with a key (key) exists for those cases, whose details will be studied in module 7. With this foundation, TaskFlow now has <task-list>, capable of walking an array of sample tasks and generating a <task-card> for each one, although still unable to pass different data to each card: that piece is deliberately left pending for module 3.

    In the last lesson of this module, "The Rendering Cycle", you'll take a step back to understand something you've been taking for granted so far: how and when Lit exactly decides when to re-run render(), why that process is asynchronous, and why you should never touch the DOM by hand inside that method, closing out the module before jumping to real reactive properties in module 3.

    Lit Course

    Module 1: Introduction to Lit and Web Components

    Module 2: Reactive Templates and Rendering

    Module 3: Reactive Properties and State

    Module 4: Styling Lit Components

    Module 5: Events and Component Communication

    Module 6: Lifecycle and Advanced Behavior

    Module 7: Directives and Advanced Template Features

    Module 8: Integration, Interoperability and Deployment

    Module 9: Testing and Best Practices

    Module 10: Project: Building TaskFlow

    © Copyright 2026. All rights reserved