Skip to content
Tools— browse docs
On this page

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 },
  ]),
})
Give it a shape
src/codec.ts + src/tool.tsx Lines 13 to 27 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: "Rope (50 ft)" and two more items, as a plain list — the seed rows from the codec, read straight off data.rows.

Note:

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.

Next steps