---
title: "Lists, Keys and Reconciliation"
description: "Lists in v6: the positional diff, identity keys, the source-item doctrine, live component-prop patching, and touch() through by-reference props."
source: https://lemonadejs.com/docs/lists/
---

# Lists and keys

A list is just an expression returning an array of views (rule 4 of the
[template language](/docs/templates/)), and matching between updates has
two modes: **positional by default, identity-based when you key the
item's root**:

```javascript
${() => rows.value.map((r) => html`<li>${r.label}</li>`)}              // positional
${() => rows.value.map((r) => html`<tr key="${r.id}">...</tr>`)}      // keyed: identity
```

`key` is a directive like `bind` and `ref`: consumed by the engine, never
rendered as an attribute, never delivered as a component prop. With a
key, the existing DOM, and any component instances inside it with their
state, **moves** instead of rebuilding.

Keyed rows keep their identity: type into an input, then shuffle. The DOM node (and your text) moves with the row.

<!--example-->

```js
import { html } from 'lemonadejs';

const App = (props, { state }) => {
    let next = 4;
    const rows = state([{ id: 1, name: 'Ana' }, { id: 2, name: 'Bruno' }, { id: 3, name: 'Carla' }]);

    const add = () => (rows.value = [...rows.value, { id: next, name: 'Person ' + next++ }]);
    const shuffle = () => (rows.value = [...rows.value].sort(() => Math.random() - 0.5));
    const remove = (r) => (rows.value = rows.value.filter((x) => x !== r));

    return html`<div>
        <button onclick="${add}">add</button> <button onclick="${shuffle}">shuffle</button>
        <ul>${() => rows.value.map((r) => html`<li key="${r.id}">
            <input value="${r.name}" /> <button onclick="${() => remove(r)}">remove</button>
        </li>`)}</ul>
    </div>`;
};
```

## Background

Rendering a list is straightforward; updating one requires consistent
answers to three questions:

- **How are old and new entries matched across updates?** Without identity
  matching, every update rebuilds the list from scratch, discarding
  component state and triggering layout.
- **What happens to component state inside a matched entry?** An entry
  rebuilt instead of moved loses focus, scroll position, and any
  in-flight edit. Identity-based matching exists so instances can move
  instead.
- **What happens when a matched entry's values changed?** In a
  setup-runs-once model this is not automatic: the instance was built
  against the old values, so the engine must carry the new ones in
  without re-running setup.

The third question required engine machinery in v6.

## Mechanics

**The positional diff is the default, and it is enough for most lists.**
Lists that only append, shrink at the end, or change in place reuse every
surviving entry by position. The engine's own suite pins this: an unkeyed
`['a','b']` → `['b','a']` keeps the same first node and just rewrites its
text. No key needed, nothing to configure.

**`key="${...}"` adds identity.** When items can reorder, be inserted or
removed, key the item's root element. Matching becomes identity-based:

```javascript
${() => rows.value.map((r) => html`<tr key="${r.id}">...</tr>`)}
key="${r}"      // the item object itself is a valid key (Object.is)
```

Keys must be unique per list; duplicates warn `LJS-204` in dev and fall
back to rebuilding (correct, but slow, and duplicate entries lose
component state).

**Key the source item, not a normalized copy.** A helper that rebuilds
item objects every render (`normalize(list).map(...)`) makes
object-identity keys churn: every update produces fresh objects, no key
ever matches, and the list fully rebuilds on every change. That is worse
than no keys at all. Normalize *per item, at render* (`asOption(raw)`)
and key by the **raw** entry, or key by a stable id.

**Key scope is one list position.** Keys match within a single list slot
only. An item moving *between* two lists (a kanban card crossing columns)
rebuilds on the other side. If
cross-list identity matters, render one flat keyed list and place items
visually (CSS grid), or accept the rebuild.

**Matched entries with changed values are patched, not rebuilt.** This is
the engine's answer to the third question:

- *Plain-element entries* update in place: bindings re-run, nodes are
  kept.
- *Entries containing components* are **patched**: new prop values flow
  into the instance's live prop states, fresh inline closures swap into
  the event handlers, and children update through their own bindings.
  DOM, internal state and focus survive; setup never re-runs.
- The instance is **rebuilt** only on a *structural* change: a different
  component in the tag, a shared `State` prop swapped for another one,
  `bind`/`ref`/`expose` changes, or an undeclared prop changing.

Patching applies to positional lists too. Keys add identity across
reorder/insert/remove, patching handles the value changes either way.

**`touch()` reaches into components.** When a list re-runs under
[touch()](/docs/state/) and an item is passed *by reference*, as in
`<${Card} item="${r}" />`, the card's prop state is touched too, so the
card re-reads the mutated contents. The propagation is recursive through
nested components (a card passing `item.value` further down). Ownership
holds: a shared `State` passed as a prop is **never** touched by
propagation; its owner touches it. This is v5's loop-scope visibility
(`self.refresh()` reaching loop items) achieved without v5's cost of
writing framework fields onto your data objects.

## In practice: the receipts

**The 200-row shuffle.** The engine suite (`tests/keys.test.ts`) reverses
a 200-row keyed list and asserts, node by node, that every element
after the shuffle *is* the element from before it, mapped by id: all 200
moved, none rebuilt. The same suite pins component-state survival: a
counter clicked twice, reordered, is the same DOM node with its count
intact.

**The datagrid keys by row identity.** Its virtualized row view is keyed
by the data object itself (the same identity its selection `Set` uses)
so a scroll keeps the overlapping window rows, and a sort or filter
*moves* surviving rows (and any component a cell `render()` mounted, with
its state) instead of rebuilding them. From the shipped source:

```javascript
// Keyed by ROW IDENTITY (the data object, the same identity the
// selection Set uses): a scroll keeps the overlapping window rows,
// a sort/filter MOVES surviving rows ... instead of rebuilding them
return html`<div key="${row}" class="lm-datagrid-row ..." role="row" ...>`;
```

**The contextmenu deleted its view cache.** Before live patching, the
contextmenu kept a `WeakMap` of one view per menu level: a rebuilt Modal
would re-run auto-adjust and visibly move the parent menu when a submenu
opened. Patching made the cache unnecessary: the engine keeps the
surviving Modal entries and flows fresh item views through the live
children bindings. The workaround was removed from the source, and the
comment marking its grave is still there.

**touch() through by-reference props**, the pattern, end to end:

```javascript
const Row = component('row', { item: Object }, (props) =>
    html`<b>${() => props.item.value.title}</b>`);

const App = (p, { state }) => {
    const rows = state([{ id: 1, title: 'a' }, { id: 2, title: 'b' }]);

    const rename = () => {
        rows.value[0].title = 'mutated';   // in place, same reference
        rows.touch();                      // the Row re-reads; no rebuild
    };

    return html`<div>
        ${() => rows.value.map((r) => html`<i key="${r.id}"><${Row} item="${r}" /></i>`)}
        <button onclick="${rename}">rename</button>
    </div>`;
};
```

The engine suite pins all three properties: the component re-reads the
mutated item without rebuilding, the propagation recurses through nested
components, and a *shared* `State` prop is left alone until its owner
touches it.

## Reference

```javascript
// Positional (default): append / shrink / in-place changes reuse entries
${() => items.value.map((x) => html`<li>${x}</li>`)}

// Keyed: reorder / insert / remove moves DOM + component instances
${() => rows.value.map((r) => html`<tr key="${r.id}">...</tr>`)}
key="${r.id}"     // stable id
key="${r}"        // the source object itself (Object.is identity)
```

- `key` is an engine directive: never rendered, never a component prop,
  no `LJS-402` on contract components.
- Unique per list; duplicates warn `LJS-204` and rebuild.
- Scope: one list position. Cross-list moves rebuild.
- Key the **source item**, never a per-render normalized copy.
- Matched entries patch: prop states written, fresh closures swapped in,
  children stay live. Structural changes (different component, swapped
  shared `State`, `bind`/`ref`/`expose`, undeclared prop) rebuild.
- `touch()` propagates into by-reference component props, recursively;
  shared `State` props stay with their owner.

For the mutation model behind `touch()`, read [State](/docs/state/); for
what a contract component's live prop states are, read
[Contracts](/docs/contracts/).