mcpField
S
sanity-form-component-suspense

sanity-form-component-suspense

github.com/sanity-io开发19462 次安装1 天前更新
代码仓库

关于 sanity-form-component-suspense

面向 Sanity Studio 开发者的指南,介绍如何处理会挂起的自定义表单组件。通过设置 Suspense 边界和匹配尺寸的占位内容,避免警告、布局跳动及内置输入框中断。

主要功能

  1. 01定位触发挂起的组件
  2. 02仅包裹异步组件
  3. 03让占位内容匹配加载后尺寸
  4. 04处理新文档与稳定 Promise
  5. 05验证新旧文档表单

适用场景

  1. 01修复 Studio 的 Suspense 警告
  2. 02避免加载时表单布局跳动
  3. 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) 的边界,它也会涵盖字段的输入控件:当一个备用内容需要同时覆盖两者时,例如输入控件会延迟加载并获取数据,就使用这种方式。

工作流程

  1. 找到组件。 警告会列出为该字段或数组项声明的自定义组件(components.field、.input 或 .item)、路径、文档类型和架构类型,并记录组件本身;在浏览器控制台中点击某个组件即可打开其源代码。生产构建会缩短组件名称,因此请根据路径和文档类型查找。如果架构类型没有声明组件,警告会以“A form component”开头,问题通常出在插件的 form.components。React DevTools 的 Suspense 标签页会显示实际暂停的组件。
  2. 找到暂停原因。 通常是以下其中一种:React.lazy、use(promise)、App SDK 钩子(useDocumentProjection、useDocument、useQuery、useDocuments),或处于 suspense 模式的库(useSuspenseQuery)。
  3. 将其移入独立组件(如果它还不是独立组件),并在自定义组件中仅用 <Suspense> 包裹该组件。将 props.renderDefault(props) 放在边界外,这样异步部分再次暂停时,内置输入控件、字段或数组项就不会卸载(用户输入时也不会失去焦点)。
  4. 预留空间。 备用内容必须与加载完成后的 UI 尺寸一致:
    • 附加 UI(警告、预览、计数):在 renderDefault 之后渲染,这样就不会把其余内容往下推;备用内容的高度应与加载完成后的一行内容相同。
    • 整个延迟加载的组件:使用高度与组件相同的骨架屏,而不是 null。
    • 数组项:列表会进行虚拟化,并测量每一行,因此如果数组项的备用内容比加载完成后的行更矮或更高,其下方的行就会跳动。请确保行高一致。
  5. 不要在文档尚不存在时暂停。 新文档只有在首次编辑创建后才会存在,而读取当前文档的 SDK 钩子会一直等待。使用 useFormValue(['_createdAt']) 进行判断:文档存在前,该值尚未设置。
  6. 保持 Promise 稳定。 在渲染期间、useState 中或 useMemo 中创建的 Promise,会在组件挂载时暂停期间被丢弃,因此每次重试时 use() 都会获取一个新的 Promise,永远无法完成。请使用 SDK 钩子、以输入为键的模块级缓存,或数据库。
  7. 验证。 打开该类型的新文档和现有文档:警告已消失,表单其余部分可立即编辑,异步部分加载后也不会发生位移。

示例

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