Weftv0.23.1
GitHub

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.

typescript
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: any Stream, Effect, or Subscribable of an Iterable. 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's Equal/Hash. Omit it and the item itself is the key (structural for Data, 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:

typescript
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