generate-frontend-forms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseForm System Guide
表单系统指南
This skill provides patterns for building forms using Sentry's new form system built on TanStack React Form and Zod validation.
本指南提供了使用基于TanStack React Form和Zod验证的Sentry新表单系统构建表单的模式。
Core Principle
核心原则
-
Always use the new form system (,
useScrapsForm) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems.AutoSaveField -
All forms should be schema based. DO NOT create a form without schema validation.
-
新表单必须使用新的表单系统(、
useScrapsForm)。绝对不要使用旧版JsonForm或基于Reflux的系统创建新表单。AutoSaveField -
所有表单都必须基于Schema。禁止创建没有Schema验证的表单。
Imports
导入
All form components are exported from :
@sentry/scraps/formtsx
import {z} from 'zod';
import {
AutoSaveField,
defaultFormOptions,
setFieldErrors,
useScrapsForm,
} from '@sentry/scraps/form';Important: DO NOT import from deeper paths, like '@sentry/scraps/form/field'. You can only use what is part of the PUBLIC interface in the index file in @sentry/scraps/form.
所有表单组件都从导出:
@sentry/scraps/formtsx
import {z} from 'zod';
import {
AutoSaveField,
defaultFormOptions,
setFieldErrors,
useScrapsForm,
} from '@sentry/scraps/form';重要提示:不要从更深层级的路径导入,例如'@sentry/scraps/form/field'。只能使用索引文件中公开的公共接口。@sentry/scraps/form
Form Hook: useScrapsForm
useScrapsForm表单Hook:useScrapsForm
useScrapsFormThe main hook for creating forms with validation and submission handling.
这是创建带有验证和提交处理功能表单的核心Hook。
Basic Usage
基础用法
tsx
import {z} from 'zod';
import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form';
const schema = z.object({
email: z.string().email('Invalid email'),
name: z.string().min(2, 'Name must be at least 2 characters'),
});
function MyForm() {
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {
email: '',
name: '',
},
validators: {
onDynamic: schema,
},
onSubmit: ({value, formApi}) => {
// Handle submission
console.log(value);
},
});
return (
<form.AppForm>
<form.FormWrapper>
<form.AppField name="email">
{field => (
<field.Layout.Stack label="Email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
<form.SubmitButton>Submit</form.SubmitButton>
</form.FormWrapper>
</form.AppForm>
);
}Important: Always spreadfirst. It configures validation to run on submit initially, then on every change after the first submission. This is why validators are defined asdefaultFormOptions, and it's what provides a consistent UX.onDynamic
tsx
import {z} from 'zod';
import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form';
const schema = z.object({
email: z.string().email('Invalid email'),
name: z.string().min(2, 'Name must be at least 2 characters'),
});
function MyForm() {
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {
email: '',
name: '',
},
validators: {
onDynamic: schema,
},
onSubmit: ({value, formApi}) => {
// Handle submission
console.log(value);
},
});
return (
<form.AppForm>
<form.FormWrapper>
<form.AppField name="email">
{field => (
<field.Layout.Stack label="Email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
<form.SubmitButton>Submit</form.SubmitButton>
</form.FormWrapper>
</form.AppForm>
);
}重要提示:务必先展开。它会配置验证逻辑,首次提交时运行验证,之后每次内容变更都会触发验证。这也是为什么验证器要定义为defaultFormOptions,以此提供一致的用户体验。onDynamic
Returned Properties
返回属性
| Property | Description |
|---|---|
| Root wrapper component (provides form context) |
| Form element wrapper (handles submit) |
| Field renderer component |
| Section grouping with title |
| Pre-wired submit button |
| Subscribe to form state changes |
| Reset form to default values |
| Manually trigger submission |
| 属性名称 | 描述 |
|---|---|
| 根容器组件(提供表单上下文) |
| 表单元素容器(处理提交逻辑) |
| 字段渲染组件 |
| 带标题的字段分组组件 |
| 预配置的提交按钮 |
| 订阅表单状态变更 |
| 将表单重置为默认值 |
| 手动触发提交 |
Field Components
字段组件
All fields are accessed via the render prop and follow consistent patterns.
field所有字段都通过渲染属性访问,并遵循一致的模式。
fieldInput Field (Text)
输入字段(文本)
tsx
<form.AppField name="firstName">
{field => (
<field.Layout.Stack label="First Name" required>
<field.Input
value={field.state.value}
onChange={field.handleChange}
placeholder="Enter your name"
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="firstName">
{field => (
<field.Layout.Stack label="First Name" required>
<field.Input
value={field.state.value}
onChange={field.handleChange}
placeholder="Enter your name"
/>
</field.Layout.Stack>
)}
</form.AppField>Number Field
数字字段
tsx
<form.AppField name="age">
{field => (
<field.Layout.Stack label="Age" required>
<field.Number
value={field.state.value}
onChange={field.handleChange}
min={0}
max={120}
step={1}
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="age">
{field => (
<field.Layout.Stack label="Age" required>
<field.Number
value={field.state.value}
onChange={field.handleChange}
min={0}
max={120}
step={1}
/>
</field.Layout.Stack>
)}
</form.AppField>Select Field (Single)
选择字段(单选)
tsx
<form.AppField name="country">
{field => (
<field.Layout.Stack label="Country">
<field.Select
value={field.state.value}
onChange={field.handleChange}
options={[
{value: 'us', label: 'United States'},
{value: 'uk', label: 'United Kingdom'},
]}
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="country">
{field => (
<field.Layout.Stack label="Country">
<field.Select
value={field.state.value}
onChange={field.handleChange}
options={[
{value: 'us', label: 'United States'},
{value: 'uk', label: 'United Kingdom'},
]}
/>
</field.Layout.Stack>
)}
</form.AppField>Select Field (Multiple)
选择字段(多选)
tsx
<form.AppField name="tags">
{field => (
<field.Layout.Stack label="Tags">
<field.Select
multiple
value={field.state.value}
onChange={field.handleChange}
options={[
{value: 'bug', label: 'Bug'},
{value: 'feature', label: 'Feature'},
]}
clearable
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="tags">
{field => (
<field.Layout.Stack label="Tags">
<field.Select
multiple
value={field.state.value}
onChange={field.handleChange}
options={[
{value: 'bug', label: 'Bug'},
{value: 'feature', label: 'Feature'},
]}
clearable
/>
</field.Layout.Stack>
)}
</form.AppField>Switch Field (Boolean)
开关字段(布尔值)
tsx
<form.AppField name="notifications">
{field => (
<field.Layout.Stack label="Enable notifications">
<field.Switch checked={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="notifications">
{field => (
<field.Layout.Stack label="Enable notifications">
<field.Switch checked={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>TextArea Field
文本域字段
tsx
<form.AppField name="bio">
{field => (
<field.Layout.Stack label="Bio">
<field.TextArea
value={field.state.value}
onChange={field.handleChange}
rows={4}
placeholder="Tell us about yourself"
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="bio">
{field => (
<field.Layout.Stack label="Bio">
<field.TextArea
value={field.state.value}
onChange={field.handleChange}
rows={4}
placeholder="Tell us about yourself"
/>
</field.Layout.Stack>
)}
</form.AppField>Range Field (Slider)
范围字段(滑块)
tsx
<form.AppField name="volume">
{field => (
<field.Layout.Stack label="Volume">
<field.Range
value={field.state.value}
onChange={field.handleChange}
min={0}
max={100}
step={10}
/>
</field.Layout.Stack>
)}
</form.AppField>tsx
<form.AppField name="volume">
{field => (
<field.Layout.Stack label="Volume">
<field.Range
value={field.state.value}
onChange={field.handleChange}
min={0}
max={100}
step={10}
/>
</field.Layout.Stack>
)}
</form.AppField>Layouts
布局
Two layout options are available for positioning labels and fields.
提供两种布局选项用于放置标签和字段。
Stack Layout (Vertical)
堆叠布局(垂直)
Label above, field below. Best for forms with longer labels or mobile layouts.
tsx
<field.Layout.Stack
label="Email Address"
hintText="We'll never share your email"
required
>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>标签在上,字段在下。适合标签较长或移动端布局的表单。
tsx
<field.Layout.Stack
label="Email Address"
hintText="We'll never share your email"
required
>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>Row Layout (Horizontal)
行布局(水平)
Label on left (~50%), field on right. Compact layout for settings pages.
tsx
<field.Layout.Row label="Email Address" hintText="We'll never share your email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>标签在左侧(约占50%),字段在右侧。适合设置页面的紧凑布局。
tsx
<field.Layout.Row label="Email Address" hintText="We'll never share your email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>Compact Variant
紧凑变体
Both Stack and Row layouts support a prop. In compact mode, the hint text appears as a tooltip on the label instead of being displayed below. This saves vertical space while still providing the hint information.
variant="compact"tsx
// Default: hint text appears below the label
<field.Layout.Row label="Email" hintText="We'll never share your email">
<field.Input ... />
</field.Layout.Row>
// Compact: hint text appears in tooltip when hovering the label
<field.Layout.Row label="Email" hintText="We'll never share your email" variant="compact">
<field.Input ... />
</field.Layout.Row>
// Also works with Stack layout
<field.Layout.Stack label="Email" hintText="We'll never share your email" variant="compact">
<field.Input ... />
</field.Layout.Stack>When to Use Compact:
- Settings pages with many fields where vertical space is limited
- Forms where hint text is supplementary, not essential
- Dashboards or panels with constrained height
堆叠布局和行布局都支持属性。在紧凑模式下,提示文本会显示为标签的工具提示,而不是在标签下方。这样可以节省垂直空间,同时仍能提供提示信息。
variant="compact"tsx
// 默认:提示文本显示在标签下方
<field.Layout.Row label="Email" hintText="We'll never share your email">
<field.Input ... />
</field.Layout.Row>
// 紧凑模式:鼠标悬停在标签上时显示提示文本的工具提示
<field.Layout.Row label="Email" hintText="We'll never share your email" variant="compact">
<field.Input ... />
</field.Layout.Row>
// 堆叠布局同样适用
<field.Layout.Stack label="Email" hintText="We'll never share your email" variant="compact">
<field.Input ... />
</field.Layout.Stack>何时使用紧凑模式:
- 字段较多、垂直空间有限的设置页面
- 提示文本为补充信息而非必需的表单
- 高度受限的仪表盘或面板
Custom Layouts
自定义布局
You are allowed to create new layouts if necessary, or not use any layouts at all. Without a layout, you should render and optionally for a11y.
field.meta.Labelfield.meta.HintTexttsx
<form.AppField name="firstName">
{field => (
<Flex gap="md">
<field.Meta.Label required>First Name:</field.Meta.Label>
<field.Input value={field.state.value ?? ''} onChange={field.handleChange} />
</Flex>
)}
</form.AppField>如有必要,你可以创建新的布局,或者不使用任何布局。如果不使用布局,你应该渲染,并可选地渲染以保证可访问性。
field.meta.Labelfield.meta.HintTexttsx
<form.AppField name="firstName">
{field => (
<Flex gap="md">
<field.Meta.Label required>First Name:</field.Meta.Label>
<field.Input value={field.state.value ?? ''} onChange={field.handleChange} />
</Flex>
)}
</form.AppField>Layout Props
布局属性
| Prop | Type | Description |
|---|---|---|
| | Field label text |
| | Helper text (below label by default, tooltip in compact mode) |
| | Shows required indicator |
| | Shows hint text in tooltip instead of below label |
| 属性 | 类型 | 描述 |
|---|---|---|
| | 字段标签文本 |
| | 辅助文本(默认在标签下方,紧凑模式下为工具提示) |
| | 显示必填指示器 |
| | 将提示文本显示为工具提示而非标签下方的文本 |
Field Groups
字段分组
Group related fields into sections with a title.
tsx
<form.FieldGroup title="Personal Information">
<form.AppField name="firstName">{/* ... */}</form.AppField>
<form.AppField name="lastName">{/* ... */}</form.AppField>
</form.FieldGroup>
<form.FieldGroup title="Contact Information">
<form.AppField name="email">{/* ... */}</form.AppField>
<form.AppField name="phone">{/* ... */}</form.AppField>
</form.FieldGroup>将相关字段分组为带标题的章节。
tsx
<form.FieldGroup title="Personal Information">
<form.AppField name="firstName">{/* ... */}</form.AppField>
<form.AppField name="lastName">{/* ... */}</form.AppField>
</form.FieldGroup>
<form.FieldGroup title="Contact Information">
<form.AppField name="email">{/* ... */}</form.AppField>
<form.AppField name="phone">{/* ... */}</form.AppField>
</form.FieldGroup>Disabled State
禁用状态
Fields accept as a boolean or string. When a string is provided, it displays as a tooltip explaining why the field is disabled.
disabledtsx
// ❌ Don't disable without explanation
<field.Input disabled value={field.state.value} onChange={field.handleChange} />
// ✅ Provide a reason when disabling
<field.Input
disabled="This feature requires a Business plan"
value={field.state.value}
onChange={field.handleChange}
/>字段接受属性,值可以是布尔值或字符串。当传入字符串时,会显示为工具提示,解释字段被禁用的原因。
disabledtsx
// ❌ 不要在没有说明的情况下禁用字段
<field.Input disabled value={field.state.value} onChange={field.handleChange} />
// ✅ 禁用时请提供原因
<field.Input
disabled="This feature requires a Business plan"
value={field.state.value}
onChange={field.handleChange}
/>Validation with Zod
使用Zod进行验证
Schema Definition
Schema定义
tsx
import {z} from 'zod';
const userSchema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.number().gte(13, 'You must be at least 13 years old'),
bio: z.string().optional(),
tags: z.array(z.string()).optional(),
address: z.object({
street: z.string().min(1, 'Street is required'),
city: z.string().min(1, 'City is required'),
}),
});tsx
import {z} from 'zod';
const userSchema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.number().gte(13, 'You must be at least 13 years old'),
bio: z.string().optional(),
tags: z.array(z.string()).optional(),
address: z.object({
street: z.string().min(1, 'Street is required'),
city: z.string().min(1, 'City is required'),
}),
});Conditional Validation
条件验证
Use for cross-field validation:
.refine()tsx
const schema = z
.object({
password: z.string(),
confirmPassword: z.string(),
})
.refine(data => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});使用进行跨字段验证:
.refine()tsx
const schema = z
.object({
password: z.string(),
confirmPassword: z.string(),
})
.refine(data => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});Conditional Fields
条件字段
Use to show/hide fields based on other field values:
form.Subscribetsx
<form.Subscribe selector={state => state.values.plan === 'enterprise'}>
{showBilling =>
showBilling ? (
<form.AppField name="billingEmail">
{field => (
<field.Layout.Stack label="Billing Email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
) : null
}
</form.Subscribe>使用根据其他字段的值显示/隐藏字段:
form.Subscribetsx
<form.Subscribe selector={state => state.values.plan === 'enterprise'}>
{showBilling =>
showBilling ? (
<form.AppField name="billingEmail">
{field => (
<field.Layout.Stack label="Billing Email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
) : null
}
</form.Subscribe>Error Handling
错误处理
Server-Side Errors
服务端错误
Use to display backend validation errors:
setFieldErrorstsx
import {useMutation} from '@tanstack/react-query';
import {setFieldErrors} from '@sentry/scraps/form';
import {fetchMutation} from 'sentry/utils/queryClient';
function MyForm() {
const mutation = useMutation({
mutationFn: (data: {email: string; username: string}) => {
return fetchMutation({
url: '/users/',
method: 'POST',
data,
});
},
});
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {email: '', username: ''},
validators: {onDynamic: schema},
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// Set field-specific errors from backend
setFieldErrors(formApi, {
email: {message: 'This email is already registered'},
username: {message: 'Username is taken'},
});
}
},
});
// ...
}Important:supports nested paths with dot notation:setFieldErrors'address.city': {message: 'City not found'}
使用显示后端验证错误:
setFieldErrorstsx
import {useMutation} from '@tanstack/react-query';
import {setFieldErrors} from '@sentry/scraps/form';
import {fetchMutation} from 'sentry/utils/queryClient';
function MyForm() {
const mutation = useMutation({
mutationFn: (data: {email: string; username: string}) => {
return fetchMutation({
url: '/users/',
method: 'POST',
data,
});
},
});
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {email: '', username: ''},
validators: {onDynamic: schema},
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// 从后端设置字段级错误
setFieldErrors(formApi, {
email: {message: 'This email is already registered'},
username: {message: 'Username is taken'},
});
}
},
});
// ...
}重要提示:支持使用点符号的嵌套路径:setFieldErrors'address.city': {message: 'City not found'}
Error Display
错误显示
Validation errors automatically show as a warning icon with tooltip in the field's trailing area. No additional code needed.
验证错误会自动显示为字段尾部区域的警告图标和工具提示,无需额外代码。
Auto-Save Pattern
自动保存模式
For settings pages where each field saves independently, use .
AutoSaveField对于每个字段独立保存的设置页面,使用。
AutoSaveFieldBasic Auto-Save Field
基础自动保存字段
tsx
import {z} from 'zod';
import {AutoSaveField} from '@sentry/scraps/form';
import {fetchMutation} from 'sentry/utils/queryClient';
const schema = z.object({
displayName: z.string().min(1, 'Display name is required'),
});
function SettingsForm() {
return (
<AutoSaveField
name="displayName"
schema={schema}
initialValue={user.displayName}
mutationOptions={{
mutationFn: data => {
return fetchMutation({
url: '/user/',
method: 'PUT',
data,
});
},
onSuccess: data => {
// Update React Query cache
queryClient.setQueryData(['user'], old => ({...old, ...data}));
},
}}
>
{field => (
<field.Layout.Row label="Display Name">
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>
)}
</AutoSaveField>
);
}tsx
import {z} from 'zod';
import {AutoSaveField} from '@sentry/scraps/form';
import {fetchMutation} from 'sentry/utils/queryClient';
const schema = z.object({
displayName: z.string().min(1, 'Display name is required'),
});
function SettingsForm() {
return (
<AutoSaveField
name="displayName"
schema={schema}
initialValue={user.displayName}
mutationOptions={{
mutationFn: data => {
return fetchMutation({
url: '/user/',
method: 'PUT',
data,
});
},
onSuccess: data => {
// 更新React Query缓存
queryClient.setQueryData(['user'], old => ({...old, ...data}));
},
}}
>
{field => (
<field.Layout.Row label="Display Name">
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>
)}
</AutoSaveField>
);
}Auto-Save Behavior by Field Type
按字段类型划分的自动保存行为
| Field Type | When it saves |
|---|---|
| Input, TextArea | On blur (when user leaves field) |
| Select (single) | Immediately when selection changes |
| Select (multiple) | When menu closes, or when X/clear clicked while menu closed |
| Switch | Immediately when toggled |
| Range | When user releases the slider, or immediately with keyboard |
| 字段类型 | 保存时机 |
|---|---|
| Input、TextArea | 失去焦点时(用户离开字段) |
| Select(单选) | 选择项变更时立即保存 |
| Select(多选) | 菜单关闭时,或菜单关闭时点击清除按钮 |
| Switch | 切换时立即保存 |
| Range | 用户释放滑块时,或使用键盘操作时立即保存 |
Auto-Save Status Indicators
自动保存状态指示器
The form system automatically shows:
- Spinner while saving (pending)
- Checkmark on success (fades after 2s)
- Warning icon on validation error (with tooltip)
表单系统会自动显示:
- 加载中图标:保存进行中(pending状态)
- 勾选图标:保存成功(2秒后消失)
- 警告图标:验证错误(带工具提示)
Confirmation Dialogs
确认对话框
For dangerous operations (security settings, permissions), use the prop to show a confirmation modal before saving. The prop accepts either a string or a function.
confirmconfirmtsx
<AutoSaveField
name="require2FA"
schema={schema}
initialValue={false}
confirm={value =>
value
? 'This will remove all members without 2FA. Continue?'
: 'Are you sure you want to allow members without 2FA?'
}
mutationOptions={{...}}
>
{field => (
<field.Layout.Row label="Require Two-Factor Auth">
<field.Switch checked={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>
)}
</AutoSaveField>Confirm Config Options:
| Type | Description |
|---|---|
| Always show this message before saving |
| Function that returns a message based on the new value, or |
Note: Confirmation dialogs always focus the Cancel button for safety, preventing accidental confirmation of dangerous operations.
Examples:
tsx
// ✅ Simple string - always confirm
confirm="Are you sure you want to change this setting?"
// ✅ Only confirm when ENABLING (return undefined to skip)
confirm={value => value ? 'Are you sure you want to enable this?' : undefined}
// ✅ Only confirm when DISABLING
confirm={value => !value ? 'Disabling this removes security protection.' : undefined}
// ✅ Different messages for each direction
confirm={value =>
value
? 'Enable 2FA requirement for all members?'
: 'Allow members without 2FA?'
}
// ✅ For select fields - confirm specific values
confirm={value => value === 'delete' ? 'This will permanently delete all data!' : undefined}对于危险操作(安全设置、权限变更),使用属性在保存前显示确认模态框。属性可以接受字符串或函数。
confirmconfirmtsx
<AutoSaveField
name="require2FA"
schema={schema}
initialValue={false}
confirm={value =>
value
? 'This will remove all members without 2FA. Continue?'
: 'Are you sure you want to allow members without 2FA?'
}
mutationOptions={{...}}
>
{field => (
<field.Layout.Row label="Require Two-Factor Auth">
<field.Switch checked={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>
)}
</AutoSaveField>确认配置选项:
| 类型 | 描述 |
|---|---|
| 保存前始终显示该消息 |
| 根据新值返回消息的函数,返回 |
注意:确认对话框始终将“取消”按钮设为焦点,防止意外确认危险操作。
示例:
tsx
// ✅ 简单字符串 - 始终确认
confirm="Are you sure you want to change this setting?"
// ✅ 仅在启用时确认(返回undefined则跳过)
confirm={value => value ? 'Are you sure you want to enable this?' : undefined}
// ✅ 仅在禁用时确认
confirm={value => !value ? 'Disabling this removes security protection.' : undefined}
// ✅ 不同操作方向显示不同消息
confirm={value =>
value
? 'Enable 2FA requirement for all members?'
: 'Allow members without 2FA?'
}
// ✅ 针对选择字段 - 确认特定值
confirm={value => value === 'delete' ? 'This will permanently delete all data!' : undefined}Form Submission
表单提交
Important: Always use TanStack Query mutations () for form submissions. This ensures proper loading states, error handling, and cache management.useMutation
重要提示:表单提交必须使用TanStack Query mutations()。这能确保正确的加载状态、错误处理和缓存管理。useMutation
Using Mutations
使用Mutations
tsx
import {useMutation} from '@tanstack/react-query';
import {fetchMutation} from 'sentry/utils/queryClient';
function MyForm() {
const mutation = useMutation({
mutationFn: (data: FormData) => {
return fetchMutation({
url: '/endpoint/',
method: 'POST',
data,
});
},
onSuccess: () => {
// Handle success (e.g., show toast, redirect)
},
});
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {...},
validators: {onDynamic: schema},
onSubmit: ({value}) => {
return mutation.mutateAsync(value).catch(() => {});
},
});
// ...
}tsx
import {useMutation} from '@tanstack/react-query';
import {fetchMutation} from 'sentry/utils/queryClient';
function MyForm() {
const mutation = useMutation({
mutationFn: (data: FormData) => {
return fetchMutation({
url: '/endpoint/',
method: 'POST',
data,
});
},
onSuccess: () => {
// 处理成功逻辑(如显示提示、跳转页面)
},
});
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {...},
validators: {onDynamic: schema},
onSubmit: ({value}) => {
return mutation.mutateAsync(value).catch(() => {});
},
});
// ...
}Submit Button
提交按钮
tsx
<Flex gap="md" justify="end">
<Button onClick={() => form.reset()}>Reset</Button>
<form.SubmitButton>Save Changes</form.SubmitButton>
</Flex>The automatically:
SubmitButton- Disables while submission is pending
- Triggers form validation before submit
tsx
<Flex gap="md" justify="end">
<Button onClick={() => form.reset()}>Reset</Button>
<form.SubmitButton>Save Changes</form.SubmitButton>
</Flex>SubmitButton- 提交进行中时禁用按钮
- 提交前触发表单验证
Do's and Don'ts
注意事项
Form System Choice
表单系统选择
tsx
// ❌ Don't use legacy JsonForm for new forms
<JsonForm fields={[{name: 'email', type: 'text'}]} />;
// ✅ Use useScrapsForm with Zod validation
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {email: ''},
validators: {onDynamic: schema},
});tsx
// ❌ 不要使用旧版JsonForm创建新表单
<JsonForm fields={[{name: 'email', type: 'text'}]} />;
// ✅ 使用带Zod验证的useScrapsForm
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {email: ''},
validators: {onDynamic: schema},
});Default Options
默认选项
tsx
// ❌ Don't forget defaultFormOptions
const form = useScrapsForm({
defaultValues: {name: ''},
});
// ✅ Always spread defaultFormOptions first
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {name: ''},
});tsx
// ❌ 不要忘记defaultFormOptions
const form = useScrapsForm({
defaultValues: {name: ''},
});
// ✅ 务必先展开defaultFormOptions
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {name: ''},
});Form Submissions
表单提交
tsx
// ❌ Don't call API directly in onSubmit
onSubmit: async ({value}) => {
await api.post('/users', value);
};
// ❌ Don't use mutateAsync without .catch() - causes unhandled rejection
onSubmit: ({value}) => {
return mutation.mutateAsync(value);
};
// ✅ Use mutations with fetchMutation and .catch(() => {})
const mutation = useMutation({
mutationFn: data => fetchMutation({url: '/users/', method: 'POST', data}),
});
onSubmit: ({value}) => {
// Return the promise to keep form.isSubmitting working
// Add .catch(() => {}) to avoid unhandled rejection - error handling
// is done by TanStack Query (onError callback, mutation.isError state)
return mutation.mutateAsync(value).catch(() => {});
};tsx
// ❌ 不要在onSubmit中直接调用API
onSubmit: async ({value}) => {
await api.post('/users', value);
};
// ❌ 不要在没有.catch()的情况下使用mutateAsync - 会导致未处理的拒绝错误
onSubmit: ({value}) => {
return mutation.mutateAsync(value);
};
// ✅ 使用带fetchMutation的mutations,并添加.catch(() => {})
const mutation = useMutation({
mutationFn: data => fetchMutation({url: '/users/', method: 'POST', data}),
});
onSubmit: ({value}) => {
// 返回Promise以保持form.isSubmitting正常工作
// 添加.catch(() => {})避免未处理的拒绝错误 - 错误处理由TanStack Query完成(onError回调、mutation.isError状态)
return mutation.mutateAsync(value).catch(() => {});
};Field Value Handling
字段值处理
tsx
// ❌ Don't use field.state.value directly when it might be undefined
<field.Input value={field.state.value} />
// ✅ Provide fallback for optional fields
<field.Input value={field.state.value ?? ''} />tsx
// ❌ 当字段值可能为undefined时,不要直接使用field.state.value
<field.Input value={field.state.value} />
// ✅ 为可选字段提供默认值
<field.Input value={field.state.value ?? ''} />Validation Messages
验证消息
tsx
// ❌ Don't use generic error messages
z.string().min(1);
// ✅ Provide helpful, specific error messages
z.string().min(1, 'Email address is required');tsx
// ❌ 不要使用通用错误消息
z.string().min(1);
// ✅ 提供有用的、具体的错误消息
z.string().min(1, 'Email address is required');Auto-Save Cache Updates
自动保存缓存更新
tsx
// ❌ Don't forget to update the cache after auto-save
mutationOptions={{
mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
}}
// ✅ Update React Query cache on success
mutationOptions={{
mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
onSuccess: (data) => {
queryClient.setQueryData(['user'], old => ({...old, ...data}));
},
}}tsx
// ❌ 自动保存后不要忘记更新缓存
mutationOptions={{
mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
}}
// ✅ 在onSuccess回调中更新React Query缓存
mutationOptions={{
mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
onSuccess: (data) => {
queryClient.setQueryData(['user'], old => ({...old, ...data}));
},
}}Auto-Save Mutation Typing with Mixed-Type Schemas
混合类型Schema的自动保存Mutation类型定义
When using with schemas that have mixed types (e.g., strings and booleans), the mutation options must be typed using the schema-inferred type. Using generic types like breaks TanStack Form's ability to narrow field types.
AutoSaveFieldRecord<string, unknown>tsx
const preferencesSchema = z.object({
theme: z.string(),
language: z.string(),
notifications: z.boolean(),
});
type Preferences = z.infer<typeof preferencesSchema>;
// ❌ Don't use generic types - breaks field type narrowing
const mutationOptions = mutationOptions({
mutationFn: (data: Record<string, unknown>) => fetchMutation({...}),
});
// ✅ Use schema-inferred type for proper type narrowing
const mutationOptions = mutationOptions({
mutationFn: (data: Partial<Preferences>) => fetchMutation({...}),
});This ensures that when you use , the field correctly infers type, and infers type.
name="theme"stringname="notifications"boolean当使用包含混合类型(如字符串和布尔值)的Schema的时,mutation选项必须使用Schema推断的类型。使用等通用类型会破坏TanStack Form的字段类型收窄能力。
AutoSaveFieldRecord<string, unknown>tsx
const preferencesSchema = z.object({
theme: z.string(),
language: z.string(),
notifications: z.boolean(),
});
type Preferences = z.infer<typeof preferencesSchema>;
// ❌ 不要使用通用类型 - 会破坏字段类型收窄
const mutationOptions = mutationOptions({
mutationFn: (data: Record<string, unknown>) => fetchMutation({...}),
});
// ✅ 使用Schema推断的类型以实现正确的类型收窄
const mutationOptions = mutationOptions({
mutationFn: (data: Partial<Preferences>) => fetchMutation({...}),
});这样确保当你使用时,字段会正确推断为类型,会推断为类型。
name="theme"stringname="notifications"booleanLayout Choice
布局选择
tsx
// ❌ Don't use Row layout when labels are very long
<field.Layout.Row label="Please enter the primary email address for your account">
// ✅ Use Stack layout for long labels
<field.Layout.Stack label="Please enter the primary email address for your account">tsx
// ❌ 标签很长时不要使用行布局
<field.Layout.Row label="Please enter the primary email address for your account">
// ✅ 标签很长时使用堆叠布局
<field.Layout.Stack label="Please enter the primary email address for your account">Quick Reference Checklist
快速参考检查清单
When creating a new form:
- Import from and
@sentry/scraps/formzod - Define Zod schema with helpful error messages
- Use with
useScrapsForm...defaultFormOptions - Set matching schema shape
defaultValues - Set
validators: {onDynamic: schema} - Wrap with and
<form.AppForm><form.FormWrapper> - Use for each field
<form.AppField> - Choose appropriate layout (Stack or Row)
- Handle server errors with
setFieldErrors - Add for submission
<form.SubmitButton>
When creating auto-save fields:
- Use component
<AutoSaveField> - Pass for validation
schema - Pass from current data
initialValue - Configure with
mutationOptionsmutationFn - Update cache in callback
onSuccess
创建新表单时:
- 从和
@sentry/scraps/form导入zod - 定义带有有用错误消息的Zod Schema
- 使用带有的
...defaultFormOptionsuseScrapsForm - 设置与Schema结构匹配的
defaultValues - 设置
validators: {onDynamic: schema} - 使用和
<form.AppForm>包裹<form.FormWrapper> - 每个字段使用
<form.AppField> - 选择合适的布局(堆叠或行)
- 使用处理服务端错误
setFieldErrors - 添加用于提交
<form.SubmitButton>
创建自动保存字段时:
- 使用组件
<AutoSaveField> - 传入用于验证
schema - 从当前数据传入
initialValue - 配置带有的
mutationFnmutationOptions - 在回调中更新缓存
onSuccess
File References
文件参考
| File | Purpose |
|---|---|
| Main form hook |
| Auto-save wrapper |
| Individual field components |
| Layout components |
| Usage examples |
| 文件路径 | 用途 |
|---|---|
| 主表单Hook |
| 自动保存包装组件 |
| 各个字段组件 |
| 布局组件 |
| 使用示例 |