<GristWidgetProvider> and <GristBoundary>
These two components form the canonical app-root pattern:
<GristWidgetProvider options={...}>
<GristBoundary>
<App />
</GristBoundary>
</GristWidgetProvider>You should mount exactly one of each, at the app root. Nesting providers is unsupported.
<GristWidgetProvider>
import { GristWidgetProvider } from "grist-widget-sdk"Wraps the children in the SDK's state machine. Internally it:
- Owns a single
GristHandshakeManagerinstance (/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,onNewRecordthrough 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
type GristWidgetProviderProps = {
options?: GristReadyOptions
children: React.ReactNode
}GristReadyOptions
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
type GristColumnDescriptor =
| string
| {
name: string
title?: string
description?: string
type?: string // "Text", "Bool", "Date", "DateTime", "Ref:Tasks", ...
optional?: boolean
allowMultiple?: boolean
strictType?: boolean
}<GristBoundary>
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
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
status | Visible UI |
|---|---|
booting | bootingFallback (default: phase-aware label from the handshake manager when mounted). |
unavailable | After unavailableGraceMs, unavailableFallback (default: panel with retry). |
error | errorFallback (default: destructive panel with retry). |
ready + gate="canRender" + !canRender | preparingFallback (default: "Preparing widget…"). |
ready (+ canRender when gated) | children. |
The grace period prevents a flicker between "booting" and "unavailable" on slow handshakes.
Custom retry surface
<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
import { useGristContext, useGristOptional } from "grist-widget-sdk"useGristContext()returns the same shape asuseGrist(), but throws when called outside<GristWidgetProvider>. Use it inside components that must live under the provider.useGristOptional()returnsUseGristResult | null. Use it in shared components that may also be rendered outside Grist.
function Maybe() {
const w = useGristOptional()
return w ? <RowPanel /> : <DocsLink />
}