Reading data
There are two read paths in the SDK:
- Subscriptions — reactive, re-render on Grist updates.
- One-shot fetches — explicit calls you trigger from event handlers or effects.
Use subscriptions for the widget's primary view; use fetches for everything else.
Subscriptions
The provider subscribes to four Grist event streams under the hood:
| Stream | Exposed as | Notes |
|---|---|---|
grist.onRecord | w.record, w.mappings | Single-row stream, decoded. |
grist.onRecords | w.records, w.recordsMappings | All visible rows in the section, decoded, row-shaped. |
grist.onOptions | w.widgetOptions, w.widgetInteraction | Persistent widget options. |
grist.onNewRecord | w.isNewRecord, w.newRecordMappings | The blank "new row" placeholder. |
Decoded means keepEncoded: false and format: "rows" — your code sees JS-native values, not ["D", 1700000000, "UTC"] tuples.
Single record
function RowPanel() {
const w = useGrist<MyRow>()
if (w.mode === "empty") return <p>Select a row.</p>
return <pre>{JSON.stringify(w.record, null, 2)}</pre>
}All records visible in the section
function Counts() {
const w = useGrist()
return <p>Visible rows: {w.records?.length ?? 0}</p>
}Mapped record
w.mappedRecord is w.record rewritten under your logical names. See Column mapping.
New-row placeholder
if (w.mode === "new-row") return <CreateRowForm mappings={w.newRecordMappings} />One-shot fetches
The hook returns several read methods. All return promises and throw if the SDK isn't ready (status !== "ready" — guard with <GristBoundary> to avoid).
| Method | Returns | Notes |
|---|---|---|
w.fetchTable(tableId) | columnar { [col]: unknown[] } | Raw docApi.fetchTable. |
w.fetchTableRows(tableId, options?) | GristRowRecord<T>[] | Decoded rows, optional safe-parse. |
w.fetchRow(tableId, rowId) | GristRowRecord<T> | null | Single row. |
w.fetchSelectedTable(options?) | columnar | Current section's table. |
w.fetchSelectedRecord(rowId, options?) | row | Current section row by id. |
w.listTables(options?) | string[] | User table ids (system/hidden tables filtered by default). Pass { includeSystem: true } for all. |
w.listColumns(tableId, options?) | GristColumnInfo[] | Column metadata (colId, type, label, …) without fetching row data. Noise columns filtered by default. |
w.getDocName() | string | Document identifier. |
fetchTableRows with decode
const rows = await w.fetchTableRows<MyTask>("Tasks", {
// Provide schema for type-aware decoding (Dates → Date, Refs → { __ref, rowId, … }).
columns: schema.columns,
order: "grist", // "grist" | "id" | "none"; default "grist" (manualSort, fallback id)
})Columns vs safeParse
These options are independent; do not pass safeParse unless you need per-cell issue tracking.
| Option | What you get |
|---|---|
columns only | Type-aware decode into plain row values (Date → Date, Ref:* → ref objects, etc.). No ok / issues wrapper on cells. |
safeParse: true (or options object) | Decode + issue tracking — each cell is a GristSafeParseCellResult (ok, typedValue, issues, …). |
| Both | Valid — the safeParse materialization path runs (same as the safe-parse example below). |
Use columns for normal reads. Add safeParse when you need to surface validation problems per cell (and optionally onSafeParseIssues).
fetchTableRows with safe parsing
When you want typed cells with per-cell issue tracking (e.g. choice/ref validation):
const parsed = await w.fetchTableRows<MyTask>("Tasks", {
columns: schema.columns,
safeParse: true, // or pass options: { refMode: "detailed", fallbackUntypedToString: true, ... }
onSafeParseIssues: (issues) => console.warn(issues),
})
// parsed: GristRowRecord<{ [K in keyof T]: GristSafeParseCellResult<T[K]> | null }>[]Each cell becomes:
type GristSafeParseCellResult<T> = {
ok: boolean
typedValue: T | null
displayValue: unknown
raw: unknown
issues: Array<{ code: string; message: string }>
}Project these back to plain rows with safeParseRowsToDisplayRows(parsed) when you only need displayValue.
Polling
The reactive w.records stream covers the current section. To watch a different table for changes, you have to poll — there is no system-wide row-change event.
The advanced hook useGristRowsFromTable does this for you:
import { useGristRowsFromTable } from "grist-widget-sdk/advanced"
const { rows, refresh, loading, error } = useGristRowsFromTable<MyRow>({
tableId: "Reports",
pollIntervalMs: 10_000,
})Schema reads for LLMs
When you need a structured snapshot of the whole document (e.g. as system context for a model), use useGristSchema() or w.buildReplicaDocumentFromDocApi(...):
const schema = useGristSchema({
excludeSystemTableData: true,
replicaRowMode: "schema+samples",
sampleRowLimit: 5,
})
console.log(schema.documentJson) // JSON string ready to feed an LLMSee useGristSchema and Replica document for the full data model.
Cell decoding helpers
For ad-hoc access to raw cells, two functions cover most cases:
import { decodeGristValue, encodeGristValue } from "grist-widget-sdk"
const cell = decodeGristValue(rawValue, columnMeta)
// Date columns → Date, Ref columns → { __ref, rowId, tableId? }, etc.
const wireValue = encodeGristValue(date, columnMeta)
// Date → epoch seconds, RefList → ["L", id1, id2, ...]For a typed parsing pipeline with issue reporting, prefer safeParseGristValues({ values, column, helperValues }).