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'.
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.
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().
You now understand keyed list rendering with repeat(). Next, learn the classMap directive for conditional CSS classes.