Skip to content

<GristWidgetProvider> and <GristBoundary>

These two components form the canonical app-root pattern:

tsx
<GristWidgetProvider options={...}>
  <GristBoundary>
    <App />
  </GristBoundary>
</GristWidgetProvider>

You should mount exactly one of each, at the app root. Nesting providers is unsupported.

<GristWidgetProvider>

tsx
import { GristWidgetProvider } from "grist-widget-sdk"

Wraps the children in the SDK's state machine. Internally it:

  • Owns a single GristHandshakeManager instance (/api/handshake) that runs the FSM (detect → negotiate → online, plus heartbeat + link tracking).
  • Publishes the manager via an internal context so slice hooks (useGristWrites, useGristSelection, …) read the same snapshot.
  • Subscribes to onRecord, onRecords, onOptions, onNewRecord through the manager's subscription effect.
  • Coalesces the heartbeat with every successful slice-hook RPC, so chatty widgets cost zero extra round-trips.

The useGrist().status you read is a projection (booting / ready / unavailable / error) of the underlying snapshot — use useGristCapabilities() from /api/handshake when you need finer gating.

Props

ts
type GristWidgetProviderProps = {
  options?: GristReadyOptions
  children: React.ReactNode
}

GristReadyOptions

ts
type GristReadyOptions = {
  requiredAccess?: "none" | "read table" | "full"
  columns?: Array<GristColumnDescriptor | string>
  allowSelectBy?: boolean
  hasCustomOptions?: boolean
  onEditOptions?: () => void

  availabilityMaxAttempts?: number  // default 30
  availabilityIntervalMs?: number   // default 200
  logWhenNotEmbedded?: boolean      // default false; controls console.debug noise
}

requiredAccess is monotonic: re-rendering with a higher level (e.g. "read table""full") re-arms grist.ready with the new level. Going lower is ignored.

GristColumnDescriptor

ts
type GristColumnDescriptor =
  | string
  | {
      name: string
      title?: string
      description?: string
      type?: string           // "Text", "Bool", "Date", "DateTime", "Ref:Tasks", ...
      optional?: boolean
      allowMultiple?: boolean
      strictType?: boolean
    }

<GristBoundary>

tsx
import { GristBoundary } from "grist-widget-sdk"

Renders fallbacks for non-ready states. Children mount when status === "ready" (and, if gate="canRender", when capabilities.canRender is true).

Props

ts
type GristBoundaryGate = "ready" | "canRender"

type GristBoundaryProps = {
  children: React.ReactNode

  /** Reads from the provider by default. Pass when not using the provider. */
  widget?: UseGristResult

  /**
   * `"ready"` (default) — children when `status === "ready"`.
   * `"canRender"` — also wait until column mappings are usable.
   */
  gate?: GristBoundaryGate

  bootingFallback?: React.ReactNode
  /** Shown when `gate="canRender"` and mappings are not yet usable. */
  preparingFallback?: React.ReactNode
  unavailableFallback?:
    | React.ReactNode
    | ((actions: GristBoundaryActions) => React.ReactNode)
  errorFallback?: (error: string, actions: GristBoundaryActions) => React.ReactNode

  /** Delay before showing the unavailable UI; smooths slow handshakes. */
  unavailableGraceMs?: number       // default 5000
}

type GristBoundaryActions = {
  reload: () => Promise<void>
  isReloading: boolean
}

Styling, layout, and copy customisation live in your own fallback components. The SDK ships sensible defaults but does not provide a configurable appearance prop — pass errorFallback={...} or unavailableFallback={...} to fully replace the chrome.

Behaviour

statusVisible UI
bootingbootingFallback (default: phase-aware label from the handshake manager when mounted).
unavailableAfter unavailableGraceMs, unavailableFallback (default: panel with retry).
errorerrorFallback (default: destructive panel with retry).
ready + gate="canRender" + !canRenderpreparingFallback (default: "Preparing widget…").
ready (+ canRender when gated)children.

The grace period prevents a flicker between "booting" and "unavailable" on slow handshakes.

Custom retry surface

tsx
<GristBoundary
  errorFallback={(error, { reload, isReloading }) => (
    <Alert variant="destructive">
      <AlertTitle>Couldn't reach Grist</AlertTitle>
      <AlertDescription>{error}</AlertDescription>
      <Button disabled={isReloading} onClick={reload}>Retry</Button>
    </Alert>
  )}
>
  <App />
</GristBoundary>

The reload function returns a promise that resolves when the SDK transitions back to ready, errors out, or proves the host is unreachable.

Using the context directly

ts
import { useGristContext, useGristOptional } from "grist-widget-sdk"
  • useGristContext() returns the same shape as useGrist(), but throws when called outside <GristWidgetProvider>. Use it inside components that must live under the provider.
  • useGristOptional() returns UseGristResult | null. Use it in shared components that may also be rendered outside Grist.
tsx
function Maybe() {
  const w = useGristOptional()
  return w ? <RowPanel /> : <DocsLink />
}

Released under the ISC License.