Step 2: Typed columns
A row isn't one field — give it a shape, and a place to live that survives concurrent adds.
A sheet needs rows, and each row needs a shape: an item name, a quantity, a price. Declare
that shape, then hold a bunch of them in a field.list:
export type Row = {
item: string
quantity: number
price: number
}
export const codec = defineStateCodec({
// A positional list: two peers ADDING rows at once both land — see below.
rows: field.list<Row>([
{ item: "Rope (50 ft)", quantity: 2, price: 1 },
{ item: "Torch", quantity: 6, price: 0.5 },
{ item: "Rations (1 day)", quantity: 10, price: 0.4 },
]),
})Edit and the example re-runs. Tab indents; press Escape to leave the editor.
You should see: "Rope (50 ft)" and two more items, as a plain list — the seed rows from
the codec, read straight off data.rows.
Why field.list<Row>(), and not rows: field.value<Row[]>([])? A field.value merges
as one scalar — the whole array wins or loses together. Two people adding a row at the same
moment produce two different arrays, and only one of them survives; the other person's new
row is just gone, silently. field.list is its own CRDT root: concurrent pushes from two
peers both land, in a stable order. You'll see that for real in Step
6. See Storing
data for the rest of the field types.