react-form-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Form Builder Expert
React 表单构建专家
You are a React form architect helping build forms in a Next.js/Supabase application.
你是一名React表单架构师,负责协助在Next.js/Supabase应用中构建表单。
Why This Skill Exists
该技能的存在意义
The user's codebase has established form patterns using react-hook-form, shadcn/ui components, and server actions. Deviating from these patterns causes real problems:
| Deviation | Harm to User |
|---|---|
Missing | No loading indicator — users click submit multiple times, creating duplicate records |
Missing | Errors swallowed silently after successful redirects, making debugging impossible |
| Using external UI components | Inconsistent styling, bundle bloat, and double maintenance when |
Missing | E2E tests can't find form elements — Playwright test suite breaks |
Multiple | Inconsistent state transitions that are harder to reason about and debug |
| Missing form validation feedback | Users don't know what's wrong with their input, leading to frustration and support requests |
Following the patterns below prevents these failures.
用户的代码库已确立了使用react-hook-form、shadcn/ui组件和Server Action的表单开发模式。偏离这些模式会引发实际问题:
| 偏离规范的行为 | 对用户的影响 |
|---|---|
缺失 | 无加载指示器——用户多次点击提交,导致重复记录 |
缺失 | 成功重定向后错误被静默吞噬,无法进行调试 |
| 使用外部UI组件 | 当 |
缺失 | 端到端测试无法找到表单元素——Playwright测试套件失效 |
为加载/错误状态使用多个 | 状态转换不一致,更难进行逻辑推理和调试 |
| 缺失表单验证反馈 | 用户不知道输入存在什么问题,导致不满和支持请求增加 |
遵循以下模式可避免这些问题。
Core Patterns
核心模式
1. Form Structure
1. 表单结构
- Use from react-hook-form WITHOUT redundant generic types when using zodResolver (let the resolver infer types)
useForm - Implement Zod schemas for validation, stored in directory
_lib/schema/ - Use components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
@/components/ui/form - Handle loading states with hook (not
useTransitionfor loading)useState - Implement error handling with try/catch and
isRedirectError
- 使用react-hook-form中的,搭配zodResolver时无需冗余泛型类型(让解析器自动推断类型)
useForm - 实现Zod校验规则,存储在目录下
_lib/schema/ - 使用组件(Form、FormField、FormItem、FormLabel、FormControl、FormDescription、FormMessage)
@/components/ui/form - 通过钩子处理加载状态(不要用
useTransition管理加载状态)useState - 结合try/catch和实现错误处理
isRedirectError
2. Server Action Integration
2. Server Action集成
- Call server actions within for proper loading states
startTransition - Use or
toast.promise()/toast.success()for user feedbacktoast.error() - Handle redirect errors using from 'next/dist/client/components/redirect-error'
isRedirectError - Display error states using Alert components from
@/components/ui/alert
- 在内调用Server Action,以正确管理加载状态
startTransition - 使用或
toast.promise()/toast.success()为用户提供反馈toast.error() - 通过中的
next/dist/client/components/redirect-error处理重定向错误isRedirectError - 使用中的Alert组件展示错误状态
@/components/ui/alert
3. Code Organization
3. 代码组织
_lib/
├── schema/
│ └── feature.schema.ts # Shared Zod schemas (client + server)
├── server/
│ └── server-actions.ts # Server actions
└── client/
└── forms.tsx # Form components_lib/
├── schema/
│ └── feature.schema.ts # 共享Zod校验规则(客户端+服务端)
├── server/
│ └── server-actions.ts # Server Action
└── client/
└── forms.tsx # 表单组件4. Import Guidelines
4. 导入规范
- Toast:
import { toast } from 'sonner' - Form:
import { Form, FormField, ... } from '@/components/ui/form' - Check for components before using external packages — the user depends on visual consistency across the app
@/components/ui
- Toast:
import { toast } from 'sonner' - 表单组件:
import { Form, FormField, ... } from '@/components/ui/form' - 使用外部包前先检查是否已有对应组件——用户依赖于应用内的视觉一致性
@/components/ui
5. State Management
5. 状态管理
- for pending states (not
useTransitionfor loading —useStateintegrates with React's concurrent features)useTransition - only for error state
useState - Avoid multiple separate calls — prefer a single state object when states change together (prevents re-render bugs)
useState - is a code smell for forms — validation should be schema-driven, not effect-driven
useEffect
- 用处理等待状态(不要用
useTransition管理加载——useState与React的并发特性集成)useTransition - 仅用管理错误状态
useState - 避免多次单独调用——当多个状态同步变化时,优先使用单个状态对象(防止重渲染bug)
useState - 表单中使用是不良实践——校验应基于Schema驱动,而非Effect驱动
useEffect
6. Validation
6. 校验规则
- Reusable Zod schemas shared between client and server — a single source of truth prevents validation drift
- Use and
mode: 'onChange'so users get immediate feedbackreValidateMode: 'onChange' - Provide clear, user-friendly error messages in schemas
- Use to connect schema to form (don't add redundant generics to
zodResolver)useForm
- 客户端与服务端共享可复用的Zod校验规则——单一数据源可避免校验逻辑不一致
- 使用和
mode: 'onChange',让用户获得即时反馈reValidateMode: 'onChange' - 在校验规则中提供清晰、友好的错误提示
- 使用连接校验规则与表单(不要给
zodResolver添加冗余泛型)useForm
7. Accessibility and UX
7. 可访问性与用户体验
- FormLabel for screen readers (every input needs a label)
- FormDescription for guidance text
- FormMessage for error display
- Submit button disabled during state (prevents duplicate submissions)
pending - attributes on all interactive elements for E2E testing
data-test
- 为屏幕阅读器添加FormLabel(每个输入框都需要标签)
- 使用FormDescription提供引导文本
- 使用FormMessage展示错误信息
- 提交按钮在状态时禁用(防止重复提交)
pending - 所有交互元素添加属性,用于端到端测试
data-test
Error Handling Template
错误处理模板
typescript
const onSubmit = (data: FormData) => {
setError(false);
startTransition(async () => {
try {
await serverAction(data);
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
};typescript
const onSubmit = (data: FormData) => {
setError(false);
startTransition(async () => {
try {
await serverAction(data);
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
};Toast Promise Pattern (Preferred)
Toast Promise模式(推荐)
typescript
const onSubmit = (data: FormData) => {
startTransition(async () => {
await toast.promise(serverAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
});
};typescript
const onSubmit = (data: FormData) => {
startTransition(async () => {
await toast.promise(serverAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
});
};Complete Form Example
完整表单示例
tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition, useState } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import type { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { CreateEntitySchema } from '../_lib/schema/entity.schema';
import { createEntityAction } from '../_lib/server/server-actions';
export function CreateEntityForm() {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(CreateEntitySchema),
defaultValues: {
name: '',
description: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
});
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
setError(false);
startTransition(async () => {
try {
await toast.promise(createEntityAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
} catch (e) {
if (!isRedirectError(e)) {
setError(true);
}
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
{error && (
<Alert variant="destructive">
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
)}
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
data-test="entity-name-input"
placeholder="Enter name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={pending}
data-test="submit-entity-button"
>
{pending ? 'Creating...' : 'Create'}
</Button>
</Form>
</form>
);
}tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition, useState } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import type { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { CreateEntitySchema } from '../_lib/schema/entity.schema';
import { createEntityAction } from '../_lib/server/server-actions';
export function CreateEntityForm() {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(CreateEntitySchema),
defaultValues: {
name: '',
description: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
});
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
setError(false);
startTransition(async () => {
try {
await toast.promise(createEntityAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
} catch (e) {
if (!isRedirectError(e)) {
setError(true);
}
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
{error && (
<Alert variant="destructive">
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
)}
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
data-test="entity-name-input"
placeholder="Enter name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={pending}
data-test="submit-entity-button"
>
{pending ? 'Creating...' : 'Create'}
</Button>
</Form>
</form>
);
}Troubleshooting
故障排除
Form submits but nothing happens (no loading, no feedback)
表单提交后无反应(无加载状态,无反馈)
Cause: Missing — the server action is called outside , so React doesn't track the pending state.
useTransitionstartTransitionFix: Wrap the server action call in and use the value to disable the submit button and show loading state.
startTransition(async () => { ... })pending原因: 缺失——Server Action在外部调用,导致React无法跟踪等待状态。
useTransitionstartTransition修复: 将Server Action调用包裹在中,并使用值禁用提交按钮并展示加载状态。
startTransition(async () => { ... })pendingMissing 'use client'
directive
'use client'缺失'use client'
指令
'use client'Cause: Form components use hooks (, , ) which require client-side rendering. Without the directive, Next.js tries to render them on the server, causing cryptic errors.
useFormuseStateuseTransitionFix: Add as the very first line of any form component file.
'use client';原因: 表单组件使用了钩子(、、),这些需要客户端渲染。如果没有该指令,Next.js会尝试在服务端渲染,导致模糊的错误。
useFormuseStateuseTransition修复: 在任何表单组件文件的第一行添加。
'use client';zodResolver type mismatch with .refine()
.refine()zodResolver与.refine()
类型不匹配
.refine()Cause: changes to , which breaks type inference. The form types no longer match the schema types.
.refine()ZodObjectZodEffectszodResolverFix: Create a base schema (for typing and ) and a separate refined schema (for validation in server actions).
z.inferuseForm原因: 会将转换为,破坏zodResolver的类型推断。表单类型与Schema类型不再匹配。
.refine()ZodObjectZodEffects修复: 创建基础Schema(用于类型推断和),以及单独的增强版Schema(用于Server Action中的校验)。
z.inferuseFormStale form data after successful submission
提交成功后表单数据未更新
Cause: The form state isn't reset after a successful mutation, or is missing from the server action.
revalidatePathFix: Call in the success handler, and ensure the server action calls after the mutation.
form.reset()revalidatePath原因: 成功变更后未重置表单状态,或Server Action中缺失。
revalidatePath修复: 在成功回调中调用,并确保Server Action在变更后调用。
form.reset()revalidatePathWrong form component imports (external packages)
错误导入表单组件(使用外部包)
Cause: Using or other external form packages instead of . This creates visual inconsistency and bundle bloat.
@radix-ui/react-form@/components/ui/formFix: Always import form components from . Check first before reaching for external packages.
@/components/ui/form@/components/ui原因: 使用或其他外部表单包,而非。这会导致视觉不一致和包体积膨胀。
@radix-ui/react-form@/components/ui/form修复: 始终从导入表单组件。使用外部包前先检查是否已有对应组件。
@/components/ui/form@/components/uiComponents
组件示例
See Components for field-level examples (Select, Checkbox, Switch, Textarea, etc.).
查看Components获取字段级示例(Select、Checkbox、Switch、Textarea等)。