Render Keyed Lists
Goal: render a list whose items reorder, insert, or remove over time, without rebuilding the whole region (which would lose focus, scroll, and input state in the surviving rows).
Use List.each, the keyed-list combinator. It renders each item once per key and reconciles across emissions — a reorder moves existing DOM nodes, an insert adds one, a remove drops one, and untouched rows are left entirely alone.
import { h, List } from "@weftui/core";
import { Stream } from "effect";
declare const rows: Subscribable.Subscribable<ReadonlyArray<{ id: number; name: string }>>;
h.ul([
List.each(
{ of: rows.changes, by: (row) => row.id }, // key by stable identity
(row) => h.li(row.name),
),
]);of— the list source: anyStream,Effect, orSubscribableof anIterable. Each emission is materialized to an array to fix order, then reconciled by key.by— projects each item to its reconciliation key, compared via Effect'sEqual/Hash. Omit it and the item itself is the key (structural forData, by reference otherwise).
Why not map?
Mapping items by hand — Stream.map(rows.changes, (rs) => rs.map(r => h.li(r.name))) — produces a new children array on every emission, so the renderer rebuilds the whole region: every row's DOM node is recreated even if only one item moved. List.each reconciles by key instead, so DOM identity (and the focus/scroll/typed-input state attached to it) survives across updates.
Refresh a row's content
Because render runs exactly once per key, reconciliation never re-runs it for a kept row — so it never refreshes that row's content on its own. To make a row's content reactive, thread a Stream inside the row rather than expecting a re-render:
List.each({ of: rows.changes, by: (row) => row.id }, (row) =>
h.li([h.span([Stream.map(row.status.changes, (s) => s)])]),
);⚠️ Index-key footgun. Keying by index (
by: (_, i) => i) reuses rows positionally, so after a reorder each position keeps its old content and you see stale rows. Prefer a stable identity key (by: (item) => item.id).
See also
List.eachAPI reference — full signature,List.Options, and the descriptor shape- Reactive Primitives — the stream-shaped sources
ofaccepts - examples/keyed-list — a runnable keyed list with reordering and a browser test