react-form-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React 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:
DeviationHarm to User
Missing
useTransition
No loading indicator — users click submit multiple times, creating duplicate records
Missing
isRedirectError
handling
Errors swallowed silently after successful redirects, making debugging impossible
Using external UI componentsInconsistent styling, bundle bloat, and double maintenance when
@/components/ui
already has the component
Missing
data-test
attributes
E2E tests can't find form elements — Playwright test suite breaks
Multiple
useState
for loading/error
Inconsistent state transitions that are harder to reason about and debug
Missing form validation feedbackUsers 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的表单开发模式。偏离这些模式会引发实际问题:
偏离规范的行为对用户的影响
缺失
useTransition
无加载指示器——用户多次点击提交,导致重复记录
缺失
isRedirectError
处理
成功重定向后错误被静默吞噬,无法进行调试
使用外部UI组件
@/components/ui
中已有对应组件时,会导致样式不一致、包体积膨胀以及双倍维护成本
缺失
data-test
属性
端到端测试无法找到表单元素——Playwright测试套件失效
为加载/错误状态使用多个
useState
状态转换不一致,更难进行逻辑推理和调试
缺失表单验证反馈用户不知道输入存在什么问题,导致不满和支持请求增加
遵循以下模式可避免这些问题。

Core Patterns

核心模式

1. Form Structure

1. 表单结构

  • Use
    useForm
    from react-hook-form WITHOUT redundant generic types when using zodResolver (let the resolver infer types)
  • Implement Zod schemas for validation, stored in
    _lib/schema/
    directory
  • Use
    @/components/ui/form
    components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
  • Handle loading states with
    useTransition
    hook (not
    useState
    for loading)
  • Implement error handling with try/catch and
    isRedirectError
  • 使用react-hook-form中的
    useForm
    ,搭配zodResolver时无需冗余泛型类型(让解析器自动推断类型)
  • 实现Zod校验规则,存储在
    _lib/schema/
    目录下
  • 使用
    @/components/ui/form
    组件(Form、FormField、FormItem、FormLabel、FormControl、FormDescription、FormMessage)
  • 通过
    useTransition
    钩子处理加载状态(不要用
    useState
    管理加载状态)
  • 结合try/catch和
    isRedirectError
    实现错误处理

2. Server Action Integration

2. Server Action集成

  • Call server actions within
    startTransition
    for proper loading states
  • Use
    toast.promise()
    or
    toast.success()
    /
    toast.error()
    for user feedback
  • Handle redirect errors using
    isRedirectError
    from 'next/dist/client/components/redirect-error'
  • Display error states using Alert components from
    @/components/ui/alert
  • startTransition
    内调用Server Action,以正确管理加载状态
  • 使用
    toast.promise()
    toast.success()
    /
    toast.error()
    为用户提供反馈
  • 通过
    next/dist/client/components/redirect-error
    中的
    isRedirectError
    处理重定向错误
  • 使用
    @/components/ui/alert
    中的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
    @/components/ui
    for components before using external packages — the user depends on visual consistency across the app
  • Toast:
    import { toast } from 'sonner'
  • 表单组件:
    import { Form, FormField, ... } from '@/components/ui/form'
  • 使用外部包前先检查
    @/components/ui
    是否已有对应组件——用户依赖于应用内的视觉一致性

5. State Management

5. 状态管理

  • useTransition
    for pending states (not
    useState
    for loading —
    useTransition
    integrates with React's concurrent features)
  • useState
    only for error state
  • Avoid multiple separate
    useState
    calls — prefer a single state object when states change together (prevents re-render bugs)
  • useEffect
    is a code smell for forms — validation should be schema-driven, not effect-driven
  • useTransition
    处理等待状态(不要用
    useState
    管理加载——
    useTransition
    与React的并发特性集成)
  • 仅用
    useState
    管理错误状态
  • 避免多次单独调用
    useState
    ——当多个状态同步变化时,优先使用单个状态对象(防止重渲染bug)
  • 表单中使用
    useEffect
    是不良实践——校验应基于Schema驱动,而非Effect驱动

6. Validation

6. 校验规则

  • Reusable Zod schemas shared between client and server — a single source of truth prevents validation drift
  • Use
    mode: 'onChange'
    and
    reValidateMode: 'onChange'
    so users get immediate feedback
  • Provide clear, user-friendly error messages in schemas
  • Use
    zodResolver
    to connect schema to form (don't add redundant generics to
    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
    pending
    state (prevents duplicate submissions)
  • data-test
    attributes on all interactive elements for E2E testing
  • 为屏幕阅读器添加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
useTransition
— the server action is called outside
startTransition
, so React doesn't track the pending state.
Fix: Wrap the server action call in
startTransition(async () => { ... })
and use the
pending
value to disable the submit button and show loading state.
原因: 缺失
useTransition
——Server Action在
startTransition
外部调用,导致React无法跟踪等待状态。
修复: 将Server Action调用包裹在
startTransition(async () => { ... })
中,并使用
pending
值禁用提交按钮并展示加载状态。

Missing
'use client'
directive

缺失
'use client'
指令

Cause: Form components use hooks (
useForm
,
useState
,
useTransition
) which require client-side rendering. Without the directive, Next.js tries to render them on the server, causing cryptic errors.
Fix: Add
'use client';
as the very first line of any form component file.
原因: 表单组件使用了钩子(
useForm
useState
useTransition
),这些需要客户端渲染。如果没有该指令,Next.js会尝试在服务端渲染,导致模糊的错误。
修复: 在任何表单组件文件的第一行添加
'use client';

zodResolver type mismatch with
.refine()

zodResolver与
.refine()
类型不匹配

Cause:
.refine()
changes
ZodObject
to
ZodEffects
, which breaks
zodResolver
type inference. The form types no longer match the schema types.
Fix: Create a base schema (for
z.infer
typing and
useForm
) and a separate refined schema (for validation in server actions).
原因:
.refine()
会将
ZodObject
转换为
ZodEffects
,破坏zodResolver的类型推断。表单类型与Schema类型不再匹配。
修复: 创建基础Schema(用于
z.infer
类型推断和
useForm
),以及单独的增强版Schema(用于Server Action中的校验)。

Stale form data after successful submission

提交成功后表单数据未更新

Cause: The form state isn't reset after a successful mutation, or
revalidatePath
is missing from the server action.
Fix: Call
form.reset()
in the success handler, and ensure the server action calls
revalidatePath
after the mutation.
原因: 成功变更后未重置表单状态,或Server Action中缺失
revalidatePath
修复: 在成功回调中调用
form.reset()
,并确保Server Action在变更后调用
revalidatePath

Wrong form component imports (external packages)

错误导入表单组件(使用外部包)

Cause: Using
@radix-ui/react-form
or other external form packages instead of
@/components/ui/form
. This creates visual inconsistency and bundle bloat.
Fix: Always import form components from
@/components/ui/form
. Check
@/components/ui
first before reaching for external packages.
原因: 使用
@radix-ui/react-form
或其他外部表单包,而非
@/components/ui/form
。这会导致视觉不一致和包体积膨胀。
修复: 始终从
@/components/ui/form
导入表单组件。使用外部包前先检查
@/components/ui
是否已有对应组件。

Components

组件示例

See Components for field-level examples (Select, Checkbox, Switch, Textarea, etc.).
查看Components获取字段级示例(Select、Checkbox、Switch、Textarea等)。