Skip to content

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

ConcernAPI
App-root setup<GristWidgetProvider options={...}>
Loading / error / unavailable UI<GristBoundary>
Primary data + writesuseGrist()
Performance-sensitive readsuseGristSelection() / useGristWrites() / useGristStatus() / useGristTheme()
Schema for LLMsuseGristSchema()
Schema mutationsgristAddColumnAction, gristAddTableAction, … + w.applyActions([...])
Decoding / encoding cellsdecodeGristValue, encodeGristValue
Safe parsing (typed cells with issue tracking)safeParseGristTableData, safeParseGristValues
RESTw.fetchWithAuth(path) / w.getAccessToken()
Attachmentsw.fetchAttachmentBlob(id) / w.fetchAttachmentBase64(id)
TestingrenderWithGrist(ui, { emulator })

Design philosophy

  1. One default surface. <GristWidgetProvider> + <GristBoundary> + useGrist() is the recommended pattern. Everything else is optional.
  2. No leaky globals. Widget code never imports grist. The SDK enforces a single grist.ready call and a single subscription per event.
  3. 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.
  4. Composable slices. Big widgets opt into slice hooks so a write does not re-render the read panel.
  5. Tests look like production. The emulator runs in-process under renderWithGrist so 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

Released under the ISC License.