Token导航 LogoToken导航

Agent Skill · 前端设计

generate-frontend-forms(生成前端表单)

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

generate-frontend-forms 是前端设计类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证信息还不明确,第三方扫描已通过,按安装量在前端设计类的 10,216 个 Skill 里排第 3,293。

总安装 1,416周安装 59GitHub Stars 43,678下载 472核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:generate-frontend-forms(生成前端表单)
来源仓库:https://github.com/getsentry/sentry
仓库路径:skills/generate-frontend-forms
安装命令:
npx skills add https://github.com/getsentry/sentry --skill generate-frontend-forms
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/getsentry/sentry --skill generate-frontend-forms

这个 Skill 是做什么的

前端设计类 Skill 关注页面结构、视觉规范和组件写法,输出通常是可运行的界面代码。

generate-frontend-forms 用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。

使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。当前顶部介绍已提供,原始 SKILL.md 摘录为空。无需额外备注。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

来源仓库内路径:skills/generate-frontend-forms

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/getsentry/sentry --skill generate-frontend-for…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

generate-frontend-forms 怎么安装?

在终端执行 npx skills add https://github.com/getsentry/sentry --skill generate-frontend-forms。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

generate-frontend-forms 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

generate-frontend-forms 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

generate-frontend-forms 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

generate-frontend-forms 用的人多吗?

公开数据是累计安装 1,416 次,近一周安装 59 次,下载 472 次,GitHub 43,678 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

generate-frontend-forms 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。
  • 来源仓库有 43,678 星,维护活跃度相对可查。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理前端设计类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

前端设计类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
frontend-design

前端设计 · Codex、Claude

8,823,360126,072只读MIT通过
vercel-composition-patterns

Vercel 组合模式 · Codex、Claude

3,858,81625,893需要联网MIT通过
vercel-react-native-skills

Vercel React native skills 前端 · Codex、Claude

2,468,66425,878只读MIT通过
adapt

adapt 搜索 · Codex、Claude

2,027,04023,125操作浏览器未明确通过
bolder

bolder 分析 · Codex、Claude

2,004,28823,130只读未明确通过
generate-frontend-forms

生成前端表单 · 本页

1,41643,678需要联网未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Form System Guide

This skill provides patterns for building forms using Sentry's new form system built on TanStack React Form and Zod validation.

Core Principle

  • Always use the new form system (useScrapsForm, AutoSaveForm) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems.
  • All forms should be schema based. DO NOT create a form without schema validation.

Imports

All form components are exported from @sentry/scraps/form:

import {z} from 'zod';

import {
  AutoSaveForm,
  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.

Form Hook: useScrapsForm

The main hook for creating forms with validation and submission handling.

Basic Usage

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={form}>
      <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.AppForm>
  );
}
Important: Always spread defaultFormOptions first. It configures validation to run on submit initially, then on every change after the first submission. This is why validators are defined as onDynamic, and it's what provides a consistent UX.

Returned Properties

PropertyDescription
AppFormRoot wrapper component (provides form context and renders <form> element). Must receive form={form} prop.
AppFieldField renderer component
FieldGroupSection grouping with title
SubmitButtonPre-wired submit button
SubscribeSubscribe to form state changes
reset()Reset form to default values
handleSubmit()Manually trigger submission

Field Components

All fields are accessed via the field render prop and follow consistent patterns.

Input Field (Text)

<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

<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)

<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)

<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)

<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

<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)

<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>

Radio Field

Radio fields use a composable API with Radio.Group and Radio.Item. Radio.Group provides group context that changes how the label is rendered for proper accessibility semantics.

Important: The layout (and its label) must be rendered *inside* Radio.Group. The group context is provided by Radio.Group, so placing the layout outside will result in incorrect accessibility semantics.
<form.AppField name="priority">
  {field => (
    <field.Radio.Group value={field.state.value} onChange={field.handleChange}>
      <field.Layout.Stack label="Priority">
        <field.Radio.Item value="low">Low</field.Radio.Item>
        <field.Radio.Item value="medium">Medium</field.Radio.Item>
        <field.Radio.Item value="high" description="Urgent issues">
          High
        </field.Radio.Item>
      </field.Layout.Stack>
    </field.Radio.Group>
  )}
</form.AppField>

For horizontal arrangement of radio items, use a Flex or Stack wrapper inside the layout:

import {Flex} from '@sentry/scraps/layout';

<field.Radio.Group value={field.state.value} onChange={field.handleChange}>
  <field.Layout.Row label="Priority">
    <Flex gap="lg">
      <field.Radio.Item value="low">Low</field.Radio.Item>
      <field.Radio.Item value="high">High</field.Radio.Item>
    </Flex>
  </field.Layout.Row>
</field.Radio.Group>;

Custom Fields with BaseField

For one-off fields that don't have a built-in component (e.g. a color picker, or any custom input), use field.Base. It provides a render prop with all the necessary accessibility and form integration props (ref, disabled, aria-invalid, aria-describedby, onBlur, name, id) that you spread onto your native element.

<form.AppField name="color">
  {field => (
    <field.Layout.Row label="Brand Color">
      <field.Base<HTMLInputElement>>
        {(baseProps, {indicator}) => (
          <Flex flexGrow={1}>
            <input
              {...baseProps}
              type="color"
              value={field.state.value}
              onChange={e => field.handleChange(e.target.value)}
            />
            {indicator}
          </Flex>
        )}
      </field.Base>
    </field.Layout.Row>
  )}
</form.AppField>

The render prop receives two arguments:

  1. baseProps — accessibility and form integration props (ref, disabled, aria-invalid, aria-describedby, onBlur, name, id) to spread onto your element
  2. {indicator} — the auto-save status indicator (spinner/checkmark) as a React node, which you can place wherever makes sense in your custom layout

The element type is inferred from the passed ref, so if you don't pass one, you have to manually annotate it with <field.Base<HTMLInputElement>>.

field.Base automatically handles:

  • Merging refs (for scroll-to-hash and external ref forwarding)
  • Disabling the field when auto-save is pending
  • Setting aria-invalid based on validation state
  • Linking to hint text via aria-describedby

Use field.Base instead of building custom wrappers that duplicate this logic. It works with any native HTML element or third-party component that accepts standard props.


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.

<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.

<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 variant="compact" 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.

// 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

Custom Layouts

You are allowed to create new layouts if necessary, or not use any layouts at all. Without a layout, you *should* render field.meta.Label and optionally field.meta.HintText for a11y.

<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

PropTypeDescription
labelstringField label text
hintTextstringHelper text (below label by default, tooltip in compact mode)
requiredbooleanShows required indicator
variant"compact"Shows hint text in tooltip instead of below label

Field Groups

Group related fields into sections with a title.

<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 disabled as a boolean or string. When a string is provided, it displays as a tooltip explaining why the field is disabled.

// ❌ 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}
/>

Validation with Zod

Schema Definition

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'),
  }),
});

Nullable Fields with Refine

When a field starts as null (e.g., a required select with no initial selection), use .nullable().refine() in the schema. This creates a difference between the schema's *input* type (which accepts null) and its *output* type (which does not). To handle this correctly:

  1. Type defaultValues explicitly as z.input<typeof schema> — this allows null as an initial value.
  2. Call schema.parse(value) inside onSubmit to narrow from z.input to z.output, stripping the null before passing to your mutation.
const schema = z.object({
  provider: z
    .enum(['GitHub', 'LaunchDarkly'])
    .nullable()
    .refine(v => v !== null, 'Provider is required'),
  name: z.string().min(1, 'Name is required'),
});

// z.input allows null for the provider field
const defaultValues: z.input<typeof schema> = {
  provider: null,
  name: '',
};

// z.output<typeof schema> has provider as non-null after refine
type FormOutput = z.output<typeof schema>;

const form = useScrapsForm({
  ...defaultFormOptions,
  defaultValues,
  validators: {onDynamic: schema},
  onSubmit: ({value}) => {
    // schema.parse narrows null away — mutation receives z.output
    return mutation.mutateAsync(schema.parse(value)).catch(() => {});
  },
});
Important: Do NOT use non-null assertions (value.provider!) or type casts to work around nullable fields. The schema.parse() approach is both type-safe and validates at runtime.

Conditional Validation

Use .refine() for cross-field validation:

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 form.Subscribe to show/hide fields based on other field values:

<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 setFieldErrors to display backend validation errors:

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: setFieldErrors supports nested paths with dot notation: '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 AutoSaveForm.

Basic Auto-Save Form

import {z} from 'zod';

import {AutoSaveForm} 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 (
    <AutoSaveForm
      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>
      )}
    </AutoSaveForm>
  );
}

Auto-Save Behavior by Field Type

Field TypeWhen it saves
Input, TextAreaOn blur (when user leaves field)
Select (single)Immediately when selection changes
Select (multiple)When menu closes, or when X/clear clicked while menu closed
SwitchImmediately when toggled
RadioImmediately when selection changes
RangeWhen user releases the slider, or immediately with keyboard

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)
Important: Do NOT use toasts to communicate auto-save status. The built-in inline indicators (spinner, checkmark, warning icon) are the correct feedback mechanism. Toasts are noisy and disruptive for fields that save frequently on every change.

Confirmation Dialogs

For dangerous operations (security settings, permissions), use the confirm prop to show a confirmation modal before saving. The confirm prop accepts either a string or a function.

<AutoSaveForm
  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>
  )}
</AutoSaveForm>

Confirm Config Options:

TypeDescription
stringAlways show this message before saving
`(value) => string \undefined`Function that returns a message based on the new value, or undefined to skip confirmation
Note: Confirmation dialogs always focus the Cancel button for safety, preventing accidental confirmation of dangerous operations.

Examples:

// ✅ 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}

Form Submission

Important: Always use TanStack Query mutations (useMutation) for form submissions. This ensures proper loading states, error handling, and cache management.

Using Mutations

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(() => {});
    },
  });

  // ...
}

Resetting After Save

When a form stays on the page after submission (e.g., settings pages), call form.reset() after a successful mutation. This re-syncs the form with updated defaultValues so it becomes pristine again — any UI that depends on the form being dirty (like conditionally shown Save/Cancel buttons) will update correctly.

onSubmit: ({value}) =>
  mutation
    .mutateAsync(value)
    .then(() => form.reset())
    .catch(() => {}),
Note: AutoSaveForm handles this automatically. You only need to add this when using useScrapsForm.

Submit Button

<Flex gap="md" justify="end">
  <Button onClick={() => form.reset()}>Reset</Button>
  <form.SubmitButton>Save Changes</form.SubmitButton>
</Flex>

The SubmitButton automatically:

  • Disables while submission is pending
  • Triggers form validation before submit

Do's and Don'ts

Form System Choice

// ❌ 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},
});

Default Options

// ❌ Don't forget defaultFormOptions
const form = useScrapsForm({
  defaultValues: {name: ''},
});

// ✅ Always spread defaultFormOptions first
const form = useScrapsForm({
  ...defaultFormOptions,
  defaultValues: {name: ''},
});

Nullable Default Values

// ❌ Don't use non-null assertions or type casts
onSubmit: ({value}) => {
  return mutation.mutateAsync({...value, provider: value.provider!});
};

// ❌ Don't skip typing defaultValues when the schema has refine
const form = useScrapsForm({
  ...defaultFormOptions,
  defaultValues: {provider: null, name: ''}, // type is inferred but imprecise
});

// ✅ Use z.input for defaultValues and schema.parse in onSubmit
const defaultValues: z.input<typeof schema> = {provider: null, name: ''};

const form = useScrapsForm({
  ...defaultFormOptions,
  defaultValues,
  validators: {onDynamic: schema},
  onSubmit: ({value}) => {
    return mutation.mutateAsync(schema.parse(value)).catch(() => {});
  },
});

Form Submissions

// ❌ 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)
  // Add .then(() => form.reset()) if the form stays on the page after save
  return mutation
    .mutateAsync(value)
    .then(() => form.reset())
    .catch(() => {});
};

Field Value Handling

// ❌ 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 ?? ''} />

Validation Messages

// ❌ Don't use generic error messages
z.string().min(1);

// ✅ Provide helpful, specific error messages
z.string().min(1, 'Email address is required');

Auto-Save Feedback

// ❌ Don't use toasts for auto-save status
mutationOptions={{
  mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
  onSuccess: () => {
    addSuccessMessage('Saved!'); // ❌ noisy and disruptive
  },
}}

// ✅ Rely on built-in inline indicators (spinner, checkmark, warning icon)
mutationOptions={{
  mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
  onSuccess: (data) => {
    queryClient.setQueryData(['user'], old => ({...old, ...data}));
    // No toast needed - AutoSaveForm shows a checkmark automatically
  },
}}

Auto-Save Cache Updates

Always update the data store or cache in onSuccess. Without this, toggling a field back to its original value won't trigger a save — TanStack Form compares against defaultValues (derived from initialValue) and skips submission when the value matches.

// ❌ 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}));
  },
}}

Auto-Save Mutation Typing

Type the mutationFn with the API's data type, not the zod schema type. The schema is for client-side field validation — the mutation should accept whatever the API endpoint accepts. Don't use generic types like Record<string, unknown> either, as that breaks TanStack Form's ability to narrow field types.

// ❌ Don't use generic types - breaks field type narrowing
const opts = mutationOptions({
  mutationFn: (data: Record<string, unknown>) => fetchMutation({...}),
});

// ❌ Don't tie mutation type to the zod schema
const opts = mutationOptions({
  mutationFn: (data: Partial<z.infer<typeof preferencesSchema>>) => fetchMutation({...}),
});

// ✅ Use the API's data type
const opts = mutationOptions({
  mutationFn: (data: Partial<UserDetails>) => fetchMutation({...}),
});

Make sure the zod schema's types are compatible with the API type. For example, if the API expects a string union like 'off' | 'low' | 'high', use z.enum(['off', 'low', 'high']) instead of z.string().

Form Reset After Save

// ❌ Don't forget to reset forms that stay on the page after save
onSubmit: ({value}) => {
  return mutation.mutateAsync(value).catch(() => {});
};

// ✅ Call form.reset() after successful save to sync with updated defaultValues
onSubmit: ({value}) => {
  return mutation
    .mutateAsync(value)
    .then(() => form.reset())
    .catch(() => {});
};

Layout Choice

// ❌ 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">

Quick Reference Checklist

When creating a new form:

  • Import from @sentry/scraps/form and zod
  • Define Zod schema with helpful error messages
  • Use useScrapsForm with ...defaultFormOptions
  • Set defaultValues matching schema shape (use z.input<typeof schema> if schema has .refine())
  • Set validators: {onDynamic: schema}
  • Wrap with <form.AppForm form={form}>
  • Use <form.AppField> for each field
  • Choose appropriate layout (Stack or Row)
  • Handle server errors with setFieldErrors
  • Add <form.SubmitButton> for submission
  • Call form.reset() after successful mutation if the form stays on the page

When creating auto-save fields:

  • Use <AutoSaveForm> component
  • Pass schema for validation
  • Pass initialValue from current data
  • Configure mutationOptions with mutationFn
  • Update cache in onSuccess callback

File References

FilePurpose
static/app/components/core/form/scrapsForm.tsxMain form hook
static/app/components/core/form/autoSaveForm.tsxAuto-save wrapper
static/app/components/core/form/field/*.tsxIndividual field components
static/app/components/core/form/layout/index.tsxLayout components
static/app/components/core/form/form.stories.tsxUsage examples

平台分布

Codex

35.06%
按下载量换算165

Claude

33.85%
按下载量换算160

Cursor

19.59%
按下载量换算92

Gemini CLI

9.47%
按下载量换算45

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills