Skip to content
Get started— browse docs
On this page

Step 5: Make it a list

A created-at timestamp gives every peer the same order, for free — and a delete button, to match.

Right now new notes land in whatever order the map happens to iterate — not wrong, exactly, but not a list either. The fix is data, not coordination: store a creation time on every note, and let each peer compute the same order from it. This step touches both halves of the pane — the // src/codec.ts and // src/tool.tsx comment lines mark where each file begins; on disk they're two separate files. (The merged import block at the top splits the same way: defineStateCodec and field belong to src/codec.ts; everything else — including module-level helpers like newId — belongs to src/tool.tsx.)

First, in src/codec.ts, give Note a creation time:

src/codec.ts
export type Note = {
  text: string
  /** Creation time — a stable sort key every peer agrees on. */
  at: number
}

Then, in src/tool.tsx, sort the entries by it inside Board, and stamp it when a note is created:

src/tool.tsx
const notes = useMemo(
  () => Object.entries(data.notes).sort((a, b) => a[1].at - b[1].at),
  [data.notes],
)
const addNote = () =>
  actions.map("notes").set(newId(), { text: "", at: Date.now() })

The useMemo isn't ceremony: sorting builds a brand-new array, and doing it inline would hand React a fresh array on every render. Memoized on data.notes, it only recomputes when a note actually changes.

Finally — still in src/tool.tsx — add a Delete button next to each textarea, inside the notes.map(…) loop, wired to actions.map("notes").delete(id). That's a new method on the hand-written actions type too: delete(key: string): void joins set in what map(name: "notes") returns — the answer shows the full type.

Order and delete
src/codec.ts + src/tool.tsx Lines 14 to 16 highlighted.

Edit and the example re-runs. Tab indents; press Escape to leave the editor.

Running · your edits, live
Starting the example…

You should see: add three or four notes, delete one from the middle — the rest keep their order. Every peer computes that order from the same at, so nobody has to agree on it over the network; it's just a read.

Note:

Why not field.list? A list merges by position — great when the sequence itself is the meaning (a timeline, a set of steps), but two people inserting at the same index at the same moment land in a defined-but-arbitrary order. A field.map keyed by id has no positions to disagree about; sorting by at gets you order back as a plain read. The Hello World starter kit has the full case for it.

Next steps