What is grist-widget-sdk?
grist-widget-sdk is a React library that wraps the Grist plugin API — the JavaScript bridge between a custom widget iframe and a Grist document. Its goal is one thing:
Make 90% of widgets a one-import, one-hook problem; keep the remaining 10% reachable without writing your own bridge.
Why this SDK?
The smallest useful widget compresses to this — provider, boundary, hook, done:
tsx
import {
GristBoundary,
GristWidgetProvider,
useGrist,
} from "grist-widget-sdk"
function Widget() {
const w = useGrist()
if (w.mode === "empty") return <p>Select a row.</p>
return <p>Row #{String(w.record!.id)}</p>
}
export default function App() {
return (
<GristWidgetProvider options={{ requiredAccess: "read table" }}>
<GristBoundary>
<Widget />
</GristBoundary>
</GristWidgetProvider>
)
}The official grist-plugin-api.js is excellent but low-level. Built directly, every widget repeats the same boilerplate:
- Polling
typeof grist !== "undefined"and detecting iframe context. - Calling
grist.ready(...)exactly once. - Subscribing to four uncomposable event streams (
onRecord,onRecords,onOptions,onNewRecord). - Reconciling decoded vs encoded payloads, columnar vs row-shaped data.
- Tracking action status / errors for writes.
- Mapping column logical names ↔ real ids both ways.
- Caching short-lived access tokens for REST calls.
- Re-doing all of the above in a way React's render cycle is happy with.
The SDK encapsulates this once. Your widget code never imports the grist global.
What it gives you
| Concern | API |
|---|---|
| App-root setup | <GristWidgetProvider options={...}> |
| Loading / error / unavailable UI | <GristBoundary> |
| Primary data + writes | useGrist() |
| Performance-sensitive reads | useGristSelection() / useGristWrites() / useGristStatus() / useGristTheme() |
| Schema for LLMs | useGristSchema() |
| Schema mutations | gristAddColumnAction, gristAddTableAction, … + w.applyActions([...]) |
| Decoding / encoding cells | decodeGristValue, encodeGristValue |
| Safe parsing (typed cells with issue tracking) | safeParseGristTableData, safeParseGristValues |
| REST | w.fetchWithAuth(path) / w.getAccessToken() |
| Attachments | w.fetchAttachmentBlob(id) / w.fetchAttachmentBase64(id) |
| Testing | renderWithGrist(ui, { emulator }) |
Design philosophy
- One default surface.
<GristWidgetProvider>+<GristBoundary>+useGrist()is the recommended pattern. Everything else is optional. - No leaky globals. Widget code never imports
grist. The SDK enforces a singlegrist.readycall and a single subscription per event. - Sane decoding defaults. Records, rows, and section reads arrive as decoded JS values (
keepEncoded: false). Opt back into raw wire formats only when you ask for them. - Composable slices. Big widgets opt into slice hooks so a write does not re-render the read panel.
- Tests look like production. The emulator runs in-process under
renderWithGristso every hook is exercised by its real code path.
When not to use this SDK
- You're not writing a React widget. (The Grist plugin API is framework-agnostic; this SDK is React-only.)
- You need to manipulate the Grist UI shell beyond what the plugin API exposes.
- You're embedding Grist inside your app (host-side use case, not widget-side).
For everything else — read tables, render records, persist widget settings, run schema migrations, sign attachments, talk to the REST API — this SDK is the recommended entry point.
Next steps
- Getting started — install and run a minimal widget.
- Demos — three live widgets you can paste into Grist today.
- Core concepts — the mental model behind selection modes, mappings, and the ready handshake.
- API reference — every exported symbol.
- Design principles — read this before proposing API changes.