Skip to content
Tools— browse docs
On this page

Step 5: Two people at once

The same board, mounted twice — pins dropped on either side both land, undamaged.

No new code this step. Below is the exact tool from Step 4 (minus the "pretend view" picker — that was a teaching device for one page, not something you'd ship), mounted in two independent panes, as if two people had it open on two different computers.

src/pin-board-tool.tsx
import type { MouseEvent as ReactMouseEvent } from "react"

import { DocumentGate, type ToolRenderProps, defineStateCodec, defineTool, field, useDocument } from "@vvd/sdk"

/** One pin. x/y are pixels in the BOARD's own space — never a scaled ancestor's. */
export type Pin = { x: number; y: number; label: string }

export const codec = defineStateCodec({
  // A per-KEY map: two people dropping different pins at once BOTH land.
  pins: field.map<Pin>(),
})

function Board({ data, actions }: {
  data: { pins: Readonly<Record<string, Pin>> }
  actions: { map(name: "pins"): { set(key: string, value: Pin): void } }
}) {
  const pins = Object.entries(data.pins)

  const onBoardClick = (e: ReactMouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect()
    actions.map("pins").set(crypto.randomUUID(), {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
      label: "Pin",
    })
  }

  return (
    <div onClick={onBoardClick} style={{ position: "relative", width: "100%", height: "100%" }}>
      {pins.map(([id, pin]) => (
        <div key={id} style={{ position: "absolute", left: pin.x, top: pin.y }}>
          📍
        </div>
      ))}
    </div>
  )
}

export function BoardView({ document }: ToolRenderProps) {
  const { data, status, actions, retry } = useDocument(
    { worldId: document.worldId, documentId: document.id },
    codec,
  )
  return (
    <DocumentGate status={status} onRetry={retry}>
      {data && actions && <Board data={data} actions={actions} />}
    </DocumentGate>
  )
}

export default defineTool({
  id: "pin-board",
  name: "pin board",
  documentTypes: ["pin-board"],
  needs: [],
  render: BoardView,
})
One board, two windows
Drop a pin in either pane.
Starting the example…

Drop a pin on the left. It appears on the right — not copied, arrived. Now drag a pin in both panes at once. Each side keeps moving the one it's holding; neither one stomps the other's position, because field.map merges by key, the same guarantee Getting Started's notes relied on.

Tip:

You didn't write any sync code, and you didn't write any conflict resolution for two people dragging different pins at once. field.map already handles it — Step 2's one-line field choice is the entire reason.

Next steps