Skip to content

Troubleshooting

Symptoms-first. If your widget is misbehaving, scan the headings — the match is usually obvious.

"Grist is not available"

You see the <GristBoundary> fallback even though the rest of your React tree renders fine.

Cause. The plugin-api script tag is missing or has not finished loading by the time React hydrates.

Fix.

  1. Confirm the script is in your app shell:

    html
    <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>

    It must be present in index.html (Vite), app/layout.tsx (Next.js), or wherever your shell lives. The SDK does not inject it for you.

  2. If the script is there, you are probably running outside a Grist iframe — open the dev URL inside a Grist Custom Widget section. The "not available" fallback is the correct behaviour in standalone mode.

w.mapBack(...) returns an empty object

You declared columns on <GristWidgetProvider> but mapBack({ X: "y" }) returns {}.

Cause. The user has not finished mapping X to a real column in the widget configuration panel.

Fix. Gate writes on w.columnMappingStatus.ok:

tsx
if (!w.columnMappingStatus.ok) {
  return (
    <p>Missing: {w.columnMappingStatus.missing.join(", ")}</p>
  )
}

If columnMappingStatus.ok === true and you still see {} for a particular field, check w.mapBackSkippedallowMultiple columns that resolve to several real columns are intentionally skipped.

table.update silently does nothing

The call resolves without throwing, but your row never changes.

Causes (in order of likelihood):

  1. Wrong fields object. You passed mapped logical names instead of physical column ids. Wrap the fields in w.mapBack({ ... }) when mappings are declared.
  2. Wrong table. w.table is the selected table. To write to a different table use w.getTable(tableId).update(...).
  3. Insufficient access. Bump requiredAccess on the provider from "read table" to "full" — read-only providers ignore writes.

Type error on w.actionStatus === "pending"

The SDK uses "idle" | "running" | "error". There is no "pending" and no "success".

Fix. Replace with "running". The action returns to "idle" on success.

useGristSchema() returns null

You call useGristSchema() and result.replicaDocument stays null.

Causes.

  • The Grist plugin api has not finished its handshake yet — wait for w.isReady === true (the schema hook auto-runs once isReady flips to true).
  • The widget does not have read table access. Schema discovery needs at least read access to enumerate tables.

theme is null indefinitely

useGrist().theme returns null even after the page is loaded.

Cause. Grist only emits theme events for widgets that are configured to receive them. Pre-Grist-1.6 widgets never see a theme update.

Fix. Treat null as "use system theme" rather than an error state. The cheat sheet pattern is:

tsx
<div data-theme={w.theme ?? "light"} />

fetchWithAuth returns 401

Authenticated REST calls fail with 401 Unauthorized.

Causes.

  1. The widget has only read table access but the endpoint needs write access. Bump requiredAccess.
  2. The cached access token expired between the call and the server receiving it. The SDK auto-retries once with a fresh token; if you still see 401, the issue is permissions, not staleness.
  3. The endpoint is not in the same Grist instance. fetchWithAuth only signs requests against the host that served the widget.

Tests fail with window.grist is undefined

Your vitest run fails inside the hook code, complaining the global is missing.

Fix. Use renderWithGrist:

tsx
import { presets, renderWithGrist } from "grist-widget-sdk/emulator/testing"

renderWithGrist(<MyWidget />, { emulator: presets.simple() })

It installs the emulator before React hydrates, so every hook sees a valid grist global. See Testing for the full pattern.

The widget re-renders too often

A deep tree re-renders on every selection change even though only one component reads the record.

Fix. Replace useGrist() with the slice hooks. Each one subscribes to a single context slice; unrelated state changes don't trigger a re-render:

tsx
const { record } = useGristSelection()
const { table } = useGristWrites()
const { status } = useGristStatus()
const { theme } = useGristTheme()

The composed useGrist() is convenient for small widgets; slices matter for big trees.

Released under the ISC License.