Sanity Form Component Suspense
SanityOfficialPublished by Sanity
- Skill
- Workflow
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This workflow guides developers in resolving React Suspense warnings within Sanity Studio custom form components. When components like inputs, fields, or items suspend without a dedicated boundary, the Studio applies a generic fallback that causes layout shifts and console warnings. By wrapping suspending logic in a specific Suspense boundary with a size-matched fallback, developers can ensure a stable, flicker-free user interface that remains responsive during data fetching.
What it does
- Identifies the source of Suspense warnings in Sanity Studio custom components.
- Provides a pattern for wrapping asynchronous logic in React Suspense boundaries.
- Ensures fallback UI elements match the dimensions of the loaded component to prevent layout shifts.
- Guides the handling of new documents to prevent infinite suspension on non-existent data.
- Maintains component stability by keeping renderDefault outside the suspension boundary.
- Offers strategies for keeping promises stable during component re-renders.
When to use it
- When the console reports that a custom input, field, or item suspended without a boundary.
- When custom form components use React.lazy, use(), or App SDK hooks like useDocumentProjection.
- When a field or array item displays a persistent skeleton loader.
- When you need to prevent layout shifts in the Studio form when async data loads.
Advantages
- Eliminates distracting layout shifts and UI flickering.
- Resolves console warnings related to unhandled suspension.
- Prevents form blocking or loss of focus during asynchronous operations.
- Improves perceived performance by providing accurate skeleton placeholders.
Usage examples
- “How do I fix a custom Sanity input that causes a layout shift when it loads data?”
- The agent provides the steps to wrap the suspending component in a Suspense boundary with a skeleton fallback that matches the component's height.
- “My custom field shows a skeleton forever on new documents, how do I fix it?”
- The agent explains how to gate the component using useFormValue(['_createdAt']) to ensure it only attempts to load after the document is created.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/sanity-io/sanity-form-component-suspense/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip sanity-form-component-suspense.zip -d .agents/skills
Documentation by Sanity
Custom form components that suspend
Studio puts a fallback boundary around every field and every array item, so one suspending
component cannot blank the form. That fallback is a generic skeleton: it does not know the size of
what it replaces, so the form shifts when the component loads, and Studio warns about it in the
console. The fix always lives in your code: declare a Suspense around the part that suspends,
with a fallback that takes the same space as what it stands in for. Any boundary you declare is
closer to what suspends than Studio's, so it always wins and nothing warns. That includes a
boundary in a custom field around props.renderDefault(props), which also covers the field's
input: use it when one fallback should cover both, for example a lazy input that also fetches.
Workflow
Find the component. The warning names the custom components declared for that field or item (
components.field,.inputor.item), the path, the document type and the schema type, and logs the components themselves; click one in the browser console to open its source. Production builds minify the names, so use the path and document type there. When the schema type declares none, the warning starts with "A form component" and the culprit is usually a plugin'sform.components. React DevTools' Suspense tab shows which component actually suspended.Find what suspends. Usually one of:
React.lazy,use(promise), an App SDK hook (useDocumentProjection,useDocument,useQuery,useDocuments), or a library in suspense mode (useSuspenseQuery).Move it into its own component if it is not already, and wrap only that component in
<Suspense>inside the custom component. Keepprops.renderDefault(props)outside the boundary, so the built-in input, field or item never unmounts (and never loses focus while someone types) when the async part suspends again.Reserve the space. The fallback must have the size of the loaded UI:
Additive UI (warnings, previews, counts): render it after
renderDefaultso it never pushes the rest down, and give the fallback the height of one loaded row.A whole component that loads lazily: a skeleton with the component's height, not
null.Array items: the list is virtualized and measures each row, so an item fallback that is shorter or taller than the loaded row makes the rows below it jump. Match the row height.
Do not suspend on a document that does not exist yet. A new document only exists once the first edit creates it, and SDK hooks that read the current document wait for it forever. Gate on
useFormValue(['_createdAt']): it is unset until the document exists.Keep promises stable. A promise created during render, in
useStateor inuseMemois thrown away while the component suspends on mount, souse()gets a new one every retry and never settles. Use the SDK hooks, a module-scope cache keyed by the inputs, or a data library.Verify. Open a new document of the type and an existing one: the warning is gone, the rest of the form is editable right away, and nothing moves when the async part lands.
Example
import {createDocumentHandle, useDocumentProjection} from '@sanity/sdk-react'
import {Card, Skeleton, Stack, Text} from '@sanity/ui'
import {Suspense} from 'react'
import {type ArrayOfObjectsInputProps, getPublishedId, useFormValue} from 'sanity'
export function ModelSizeInput(props: ArrayOfObjectsInputProps) {
const documentId = useFormValue(['_id']) as string
const documentExists = Boolean(useFormValue(['_createdAt']))
const hasModels = (props.value?.length ?? 0) > 0
return (
<Stack gap={3}>
{props.renderDefault(props)}
{documentExists && hasModels ? (
<Suspense fallback={<Skeleton radius={2} style={{height: 33}} animated />}>
<ModelSizeWarnings documentId={getPublishedId(documentId)} />
</Suspense>
) : null}
</Stack>
)
}
function ModelSizeWarnings(props: {documentId: string}) {
const {data} = useDocumentProjection<{missing: number}>({
...createDocumentHandle({documentType: 'product', documentId: props.documentId}),
projection: `{"missing": count(models[!defined(@->size)])}`,
})
return (
<Card padding={2} radius={2} tone={data.missing > 0 ? 'critical' : 'positive'}>
<Text size={1}>{data.missing} models are missing a size</Text>
</Card>
)
}The skeleton's height matches the one-line card it stands in for. If the loaded UI's height varies, reserve the most common case and keep the variable part below the input.
Files
- ATTRIBUTION.mdAdded by AutoGineer891 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md5.3 KiB
ATTRIBUTION.md
Attribution
This folder is an unmodified copy of the Agent Skill "sanity-form-component-suspense", distributed by AutoGineer (https://autogineer.com/agent-skills).
Commit: 3ed3a143f8d3413faa7790ac0b2274fd3a8a6320
Published by: Sanity (https://github.com/sanity-io)
Publisher: Sanity (https://github.com/sanity-io)
Licence: MIT License (https://github.com/sanity-io/agent-toolkit/blob/3ed3a143f8d3413faa7790ac0b2274fd3a8a6320/LICENSE)
Licence evidence: repository licence LICENSE
Retrieved: 2026-11-07
The skill's own files are unmodified copies. AutoGineer is not affiliated with or endorsed by the author or the publisher.
Files added by AutoGineer
ATTRIBUTION.md: this file, written by AutoGineer
LICENSE: copied from LICENSE
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Sanity
- Publisher
- Sanity (Official)
- Source
- sanity-io/agent-toolkit · skills/sanity-form-component-suspense · commit 3ed3a14
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sanity.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download sanity-form-component-suspense.zip