sanity-form-component-suspense について
Sanity Studio のカスタムフォームコンポーネントで発生するサスペンドへの対処ガイドです。Suspense 境界と表示領域に合うフォールバックを設定し、警告やレイアウトのずれ、組み込み入力欄への影響を防ぎます。
主な機能
- 01サスペンド元のコンポーネント特定
- 02非同期コンポーネントのみをラップ
- 03読み込み後のUIに合うサイズのフォールバック
- 04新規ドキュメントと安定したPromiseへの対応
- 05新規・既存フォームでの検証
活用シーン
- 01StudioのSuspense警告の修正
- 02読み込み中のフォームレイアウトずれの防止
- 03非同期UI読み込み中の入力欄の維持
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ sanity-io/ agent-toolkit --skill sanity-form-component-suspenseインストール後、Claude Code で/sanity-form-component-suspenseを入力して読み込まれたか確認します。
詳細
サスペンドするカスタムフォームコンポーネント
Studio は、フォーム全体が空白にならないよう、すべてのフィールドと配列アイテムの周囲にフォールバック境界を設けています。このフォールバックは汎用的なスケルトンです。置き換える内容のサイズを把握していないため、コンポーネントの読み込み時にフォームがずれ、Studio はコンソールに警告を表示します。解決策は常にコード側にあります。サスペンドする部分を Suspense で囲み、置き換え対象と同じスペースを占めるフォールバックを指定してください。独自に宣言した境界は、Studio の境界よりもサスペンドする箇所に近いため、常に優先され、警告も表示されません。これには、props.renderDefault(props) を囲むカスタムフィールド内の境界も含まれます。この境界はフィールドの入力も対象にするため、たとえば遅延読み込みされ、同時にデータも取得する入力など、1 つのフォールバックで両方をカバーしたい場合に使用します。
ワークフロー
- コンポーネントを見つけます。 警告には、そのフィールドまたはアイテムに対して宣言されたカスタムコンポーネント(
components.field、.input、.item)、パス、ドキュメントタイプ、スキーマタイプが表示され、コンポーネント自体もログに記録されます。ブラウザーのコンソールでコンポーネントをクリックすると、ソースを開けます。本番ビルドでは名前が最小化されるため、パスとドキュメントタイプを手がかりにしてください。スキーマタイプにコンポーネントが宣言されていない場合、警告は「A form component」で始まり、原因は通常、プラグインのform.componentsです。React DevTools の Suspense タブでは、実際にサスペンドしたコンポーネントを確認できます。 - サスペンドする箇所を見つけます。 通常は、次のいずれかです。
React.lazy、use(promise)、App SDK のフック(useDocumentProjection、useDocument、useQuery、useDocuments)、またはサスペンスモードのライブラリ(useSuspenseQuery)。 - まだ独立したコンポーネントになっていない場合は、そこへ移動します。 そのコンポーネントだけを、カスタムコンポーネント内で
<Suspense>で囲みます。props.renderDefault(props)は境界の外に置いてください。そうすれば、非同期部分が再びサスペンドしても、組み込みの入力、フィールド、アイテムがアンマウントされず(入力中にフォーカスが失われることもなくなり)ます。 - スペースを確保します。 フォールバックは、読み込み後の UI と同じサイズにしてください。
- 追加表示する UI(警告、プレビュー、件数など):
renderDefaultの後に描画すれば、残りの部分を下に押し下げることはありません。フォールバックの高さは、読み込み後の 1 行分にします。 - 全体が遅延読み込みされるコンポーネント:
nullではなく、コンポーネントと同じ高さのスケルトンを使用します。 - 配列アイテム:リストは仮想化され、各行のサイズが測定されます。そのため、アイテムのフォールバックが読み込み後の行より低くても高くても、下の行がずれてしまいます。行の高さを一致させてください。
- 追加表示する UI(警告、プレビュー、件数など):
- まだ存在しないドキュメントでサスペンドしないようにします。 新しいドキュメントは最初の編集で作成されるまで存在せず、現在のドキュメントを読み取る SDK フックは作成されるまで待ち続けます。
useFormValue(['_createdAt'])で条件分岐してください。この値は、ドキュメントが存在するまで未設定です。 - プロミスを安定させます。 レンダリング中、
useState内、またはuseMemo内で作成したプロミスは、コンポーネントがマウント時にサスペンドすると破棄されます。そのため、再試行のたびにuse()は新しいプロミスを受け取り、完了しません。SDK フック、入力値をキーとするモジュールスコープのキャッシュ、またはデータライブラリを使用してください。 - 確認します。 対象のドキュメントタイプで、新しいドキュメントと既存のドキュメントを開きます。警告が消え、フォームの残りの部分をすぐに編集でき、非同期部分が読み込まれても何も動かないことを確認してください。
例
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>
)
}
スケルトンの高さは、置き換える 1 行のカードと一致しています。読み込み後の UI の高さが変動する場合は、最も一般的なケースのスペースを確保し、変動する部分は入力の下に配置してください。
コメント
Markdown 対応