Skip to content
Guides— browse docs
On this page

Retrieving and changing data

useDocument gives you a typed snapshot, typed mutators, and a connection status — and that is the whole read/write API.

You have a codec. Now you need to render it and change it — and you need an answer to "what do I show while it's still connecting?", because a real document arrives over a network and sometimes doesn't arrive at all.

One hook covers all of it.

You will learn

  • How to read a document as typed, reactive data
  • Every mutator: set, update, list(n), map(n), transact
  • How to handle connecting, disconnected, and read-only without writing that UI

Before you start: this page continues the codec from Storing data.

useDocument

src/tool.tsx
import { DocumentGate, defineTool, useDocument } from "@vvd/sdk"
import { codec } from "@/codec"

export default defineTool({
  id: "bestiary",
  name: "Bestiary",
  documentTypes: ["bestiary"],
  render: function View({ document, context }) {
    const coords = { worldId: document.worldId, documentId: document.id }
    const { data, status, actions, handle, retry } = useDocument(coords, codec)

    return (
      <DocumentGate status={status} onRetry={retry}>
        {data && actions && (
          <input
            value={data.name}
            disabled={!context.canEdit}
            onChange={(e) => actions.set("name", e.target.value)}
          />
        )}
      </DocumentGate>
    )
  },
})

Five things come back:

What it is
dataA typed, reactive snapshot of your codec's shape. Re-renders on any change — yours or someone else's. null until the document is open.
actionsTyped mutators. Every one of them is collaborative. null until the document is open.
statusconnecting · ready · disconnected · auth_error · error.
handleAn opaque handle you pass to presence hooks and to <CollaborativeText>.
retryRebuilds the connection. Hand it to <DocumentGate> and you're done.
Warning:

Memoise coords, or pass the same object identity between renders. A fresh { worldId, documentId } literal on every render is a new object, and hooks that depend on it will do more work than they need to. In practice:

src/tool.tsx
const coords = useMemo(
  () => ({ worldId: document.worldId, documentId: document.id }),
  [document.worldId, document.id],
)

<DocumentGate> handles the states you'd rather not write

Wrap your UI in it and you inherit a skeleton while connecting, a readable error with a retry button when the connection drops, and the right message when the reader isn't allowed to open the document. Every tool on the platform gets the same treatment, so users learn it once.

You can render your own skeleton by passing Skeleton to defineTool — but you should not be writing connection UI in your view.

The actions

Five mutators, all typed against your codec. You can't set a list, and you can't push to a scalar — the types won't let you.

src/tool.tsx
actions.set("name", "Mire Stalker")               // a value field
actions.update("threat", (n) => n + 1)            // read-modify-write, one transaction

actions.list("traits").push("Amphibious")         // append
actions.list("traits").insert(0, "Ambush hunter") // at an index
actions.list("traits").remove(2)                  // remove(index, count = 1)
actions.list("traits").replace(1, "Nocturnal")    // swap one item
actions.list("traits").move(3, 0)                 // reorder

actions.map("stats").set("speed", 40)             // a keyed entry
actions.map("stats").delete("speed")

actions.transact(() => {                          // ONE undo step, one broadcast
  actions.set("name", "Unknown creature")
  actions.list("traits").remove(0, 99)
})

transact

Reach for it whenever a single user gesture makes several changes. Without it, "clear this entry" is five separate edits, and Cmd-Z undoes them one at a time — which reads as a bug to the person pressing it. actions.transact(() => { ... }) above batches the reset into one undo step and one broadcast, no matter how many mutators run inside it.

All of it, running

Every button below calls one of those mutators. Change the code and the tool rebuilds.

Every mutator, live
src/tool.tsx

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

Running · your edits, live
Starting the example…

Always gate writes on context.canEdit

context.canEdit is the platform's answer to "may this person change this document?" — it folds in world membership, per-document sharing, published read-only views, and live permission changes pushed from the server while the tab is open.

Disable your controls with it. Don't compute permission yourself, and don't assume that because the document opened, it's editable:

src/tool.tsx
<button disabled={!context.canEdit} onClick={() => actions.list("traits").push("New")}>

Reading is reactive, not fetched

data is a live snapshot. When somebody else edits the document — in another tab, on another continent — your component re-renders with the new value. There is nothing to subscribe to, no invalidation, no refetch, and no cache to reason about.

It also re-renders only when your codec's data actually changed. An unrelated edit in another part of the document doesn't cost you a render.

When it doesn't work

data and actions are null and stay null. The document hasn't opened. Look at status: connecting is normal for a moment; auth_error means the reader can't open this document; error is worth showing with <DocumentGate>, which gives them a retry button.

Nothing happens when you call an action. Check context.canEdit. A read-only host accepts the call and drops the write rather than throwing, so a tool that never checks canEdit looks silently broken.

Undo undoes one tiny piece at a time. Wrap the gesture in actions.transact(() => { … }).

Your input loses the cursor on every keystroke. You're re-creating coords (or an extensions array) on every render. Memoise it.

Recap

  • useDocument(coords, codec) returns { data, actions, status, retry, handle } and that is the whole read/write API.
  • actions.set, .update, .list(n), .map(n) and .transact are the mutators; each one is already collaborative.
  • <DocumentGate> covers connecting, error and retry, so you never write connection UI.
  • data and actions are null until the document is ready — that's the one branch every tool has.

Next steps