关于 sanity-form-component-suspense
面向 Sanity Studio 开发者的指南,介绍如何处理会挂起的自定义表单组件。通过设置 Suspense 边界和匹配尺寸的占位内容,避免警告、布局跳动及内置输入框中断。
主要功能
- 01定位触发挂起的组件
- 02仅包裹异步组件
- 03让占位内容匹配加载后尺寸
- 04处理新文档与稳定 Promise
- 05验证新旧文档表单
适用场景
- 01修复 Studio 的 Suspense 警告
- 02避免加载时表单布局跳动
- 03异步内容加载时保持输入框挂载
安装
复制下面的命令,在终端中运行。
$
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) 的边界,它也会涵盖字段的输入控件:当一个备用内容需要同时覆盖两者时,例如输入控件会延迟加载并获取数据,就使用这种方式。
工作流程
- 找到组件。 警告会列出为该字段或数组项声明的自定义组件(
components.field、.input或.item)、路径、文档类型和架构类型,并记录组件本身;在浏览器控制台中点击某个组件即可打开其源代码。生产构建会缩短组件名称,因此请根据路径和文档类型查找。如果架构类型没有声明组件,警告会以“A form component”开头,问题通常出在插件的form.components。React DevTools 的 Suspense 标签页会显示实际暂停的组件。 - 找到暂停原因。 通常是以下其中一种:
React.lazy、use(promise)、App SDK 钩子(useDocumentProjection、useDocument、useQuery、useDocuments),或处于 suspense 模式的库(useSuspenseQuery)。 - 将其移入独立组件(如果它还不是独立组件),并在自定义组件中仅用
<Suspense>包裹该组件。将props.renderDefault(props)放在边界外,这样异步部分再次暂停时,内置输入控件、字段或数组项就不会卸载(用户输入时也不会失去焦点)。 - 预留空间。 备用内容必须与加载完成后的 UI 尺寸一致:
- 附加 UI(警告、预览、计数):在
renderDefault之后渲染,这样就不会把其余内容往下推;备用内容的高度应与加载完成后的一行内容相同。 - 整个延迟加载的组件:使用高度与组件相同的骨架屏,而不是
null。 - 数组项:列表会进行虚拟化,并测量每一行,因此如果数组项的备用内容比加载完成后的行更矮或更高,其下方的行就会跳动。请确保行高一致。
- 附加 UI(警告、预览、计数):在
- 不要在文档尚不存在时暂停。 新文档只有在首次编辑创建后才会存在,而读取当前文档的 SDK 钩子会一直等待。使用
useFormValue(['_createdAt'])进行判断:文档存在前,该值尚未设置。 - 保持 Promise 稳定。 在渲染期间、
useState中或useMemo中创建的 Promise,会在组件挂载时暂停期间被丢弃,因此每次重试时use()都会获取一个新的 Promise,永远无法完成。请使用 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>
)
}
骨架屏的高度与其所替代的单行卡片相同。如果加载完成后的 UI 高度不固定,请预留最常见情况的空间,并将可变部分放在输入控件下方。
评论
支持 Markdown