Skip to content

The repeat() Directive

repeat() is the directive for rendering lists whose items can be reordered, filtered, or contain internal state. This lesson covers its signature and exactly how it differs from plain .map().

Keyed List Rendering

repeat(items, keyFunction, templateFunction) renders a list the same way .map() does visually, but tracks each rendered DOM node by the key returned from keyFunction, rather than by its position in the array. When the array is reordered, Lit moves the existing DOM nodes to match the new order instead of destroying and recreating them.

This distinction matters most for lists containing focusable elements, CSS transitions, or any per-item internal state — anything where 'this DOM node is actually the same logical item, just moved' is a meaningfully different outcome than 'this DOM node was destroyed and a new one created in its place'.

import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';

render() {
  return html`
    <ul>
      ${repeat(
        this.items,
        (item) => item.id,
        (item, index) => html`<li>${index + 1}. ${item.name}</li>`
      )}
    </ul>
  `;
}

The templateFunction also receives the current index as a second argument, useful for numbering even though the key itself is item.id, not the index.

repeat() Signature

repeat(
  items: Iterable<T>,
  keyFunction: (item: T, index: number) => unknown,
  templateFunction: (item: T, index: number) => TemplateResult
): DirectiveResult
  • keyFunction must return a stable, unique value per item — a database id or UUID, never the array index.
  • templateFunction receives both the item and its current index, in case the index is needed for display purposes only.
  • repeat() is imported from lit/directives/repeat.js, not from the core lit package.
  • Without repeat(), plain .map() diffing is positional, which is fine for static or append-only lists but not for reorderable ones.

repeat() vs map() Cheat Sheet

Deciding when the extra ceremony of repeat() is worth it.

Scenario map() repeat()
Static list, never changes Fine Unnecessary overhead
Append-only list Fine Unnecessary overhead
Sortable/filterable list Can misattribute state Correctly reuses/moves DOM nodes
List with focusable inputs per row Focus can jump unexpectedly Focus stays with the correct item
List with per-item CSS transitions Transitions can misfire Transitions track the correct item

A Concrete Reordering Example

Consider a sortable list of people with an editable text input per row. Sorting the underlying array by name should move each input along with its correct value — not leave inputs in their original positions with mismatched values.

@state() private people = [
  { id: 1, name: 'Grace' },
  { id: 2, name: 'Ada' },
];

#sortByName() {
  this.people = [...this.people].sort((a, b) => a.name.localeCompare(b.name));
}

render() {
  return html`
    ${repeat(this.people, (p) => p.id, (p) => html`
      <input .value=${p.name} />
    `)}
  `;
}

With repeat(), each <input> DOM node (and any text the user was mid-typing) correctly follows its person after sorting, because it's tracked by p.id, not by array position.

repeat() Has a Small Overhead — Use It Deliberately

Tracking items by key requires slightly more bookkeeping than plain positional diffing, so repeat() isn't a strictly free upgrade over .map(). For lists that are genuinely static or only ever appended to, plain .map() remains the simpler, equally correct, and marginally cheaper choice.

Common Mistakes

  • Passing the array index as the key function's return value, which defeats the entire purpose of keyed diffing.
  • Reaching for repeat() by default even for simple, static, or append-only lists where plain .map() would be simpler.
  • Using a key that isn't actually unique across the list (e.g. a non-unique name field), causing Lit to misattribute DOM nodes.
  • Forgetting to import repeat from its specific lit/directives/repeat.js module path.

Key Takeaways

  • repeat(items, keyFn, templateFn) renders a list tracked by stable key rather than array position.
  • It correctly moves existing DOM nodes (and their internal state) on reorder, instead of recreating them.
  • The key function must return a genuinely stable, unique identifier — never the array index.
  • Reserve repeat() for lists that reorder, filter, or contain stateful/focusable elements; plain .map() is fine otherwise.

Pro Tip

If you're ever unsure whether a list needs repeat(), ask: 'if I sort or filter this array, should any element with internal state (an input's typed value, a CSS transition, focus) follow its logical item?' If yes, use repeat().