Tutorial: Reorderable List
You'll learn: how keyed each moves nodes (preserving DOM state) instead of rebuilding. Time: 6 minutes.
import { html, mount, each, createState } from "../src/index.js";
const items = createState(
[1, 2, 3, 4, 5].map((n) => ({ id: n, label: `Item ${n}` }))
);
const reverse = () => items.set([...items.get()].reverse());
const shuffle = () => {
const next = [...items.get()];
for (let i = next.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[next[i], next[j]] = [next[j], next[i]];
}
items.set(next);
};
function App() {
return html`
<div>
<button onclick=${shuffle}>Shuffle</button>
<button onclick=${reverse}>Reverse</button>
<ul>${each(
() => items.get(),
(it) => it.id,
(it) => html`<li>${it.label} <input placeholder="type here" /></li>`
)}</ul>
</div>
`;
}
mount(App, document.querySelector("#app"));The thing to notice#
Type something into one of the inputs, then click Reverse or Shuffle. Your text — and the focus — stay with the item.
That's because each matched items by it.id and moved the existing <li> nodes into their new positions instead of recreating them. A naïve "rebuild the list" approach would throw away your inputs.
This is the whole point of keyed reconciliation:
- unchanged keys → nodes reused,
- reordered keys → nodes moved,
- new keys → nodes inserted,
- removed keys → nodes disposed (and their effects/listeners cleaned up).
Try it yourself#
- Add a "Sort A→Z" button (sort a copy by
label, thenset). - Add a per-item "Remove" button and watch its node (and any typed text) disappear cleanly.
- Compare with a deliberately-broken version that keys by index — notice inputs jump between rows.
Back to docs home or the examples index.