mcpField
R
react-forms

react-forms

github.com/claude-dev-suite开发4642 次安装4 天前更新
代码仓库

关于 react-forms

面向 React 开发者的表单模式技能,涵盖受控与非受控输入、React Hook Form 和验证实践,并提供常见问题排查建议。

主要功能

  1. 01受控与非受控表单
  2. 02React Hook Form 用法
  3. 03表单数组与受控组件
  4. 04验证和可访问性实践
  5. 05提交问题排查

适用场景

  1. 01选择适合需求的输入管理方式
  2. 02为表单设置模式验证
  3. 03处理动态表单字段
  4. 04排查提交或验证故障

MCP 相关

提及通过 mcp__documentation__fetch_docs 获取 React 表单文档。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-forms

安装后在 Claude Code 中输入/react-forms确认已加载。

附带文件advanced.md

详细介绍

React 表单

深入了解:使用 mcp__documentation__fetch_docs,指定 technology:react、topic:forms,获取有关 React 表单模式和验证策略的完整文档。

完整参考:参阅 advanced.md,了解 Server Actions、多步表单、依赖字段、自动保存、验证模式和无障碍功能。

受控与非受控

受控表单

由 React 状态控制输入:

function ControlledForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));

    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    const newErrors: Record<string, string> = {};
    if (!formData.email) newErrors.email = 'Email required';
    if (!formData.password) newErrors.password = 'Password required';

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    console.log('Submitting:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}
非受控表单(useRef)

由 DOM 控制输入:

function UncontrolledForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    const data = {
      email: emailRef.current?.value,
      password: passwordRef.current?.value,
    };

    console.log('Submitting:', data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} name="email" type="email" />
      <input ref={passwordRef} name="password" type="password" />
      <button type="submit">Submit</button>
    </form>
  );
}
FormData API(非受控)
function FormDataForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    const data = {
      email: formData.get('email') as string,
      password: formData.get('password') as string,
    };

    console.log('Submitting:', data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Submit</button>
    </form>
  );
}

React Hook Form

最受欢迎的表单库——采用非受控方式,开发体验出色:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords must match',
  path: ['confirmPassword'],
});

type FormData = z.infer<typeof schema>;

function SignUpForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '', confirmPassword: '' },
  });

  const onSubmit = async (data: FormData) => {
    try {
      await signUp(data);
      reset();
    } catch (error) {
      console.error('Sign up failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('email')} type="email" placeholder="Email" />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <input {...register('password')} type="password" placeholder="Password" />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>

      <div>
        <input {...register('confirmPassword')} type="password" placeholder="Confirm Password" />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Signing up...' : 'Sign Up'}
      </button>
    </form>
  );
}
表单数组
import { useFieldArray, useForm } from 'react-hook-form';

interface OrderForm {
  items: { productId: string; quantity: number }[];
  notes: string;
}

function OrderForm() {
  const { register, control, handleSubmit } = useForm<OrderForm>({
    defaultValues: {
      items: [{ productId: '', quantity: 1 }],
      notes: '',
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'items',
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id} className="flex gap-2">
          <select {...register(`items.${index}.productId`)}>
            <option value="">Select product</option>
            {products.map(p => (
              <option key={p.id} value={p.id}>{p.name}</option>
            ))}
          </select>

          <input
            {...register(`items.${index}.quantity`, { valueAsNumber: true })}
            type="number"
            min="1"
          />

          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}

      <button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
        Add Item
      </button>

      <textarea {...register('notes')} placeholder="Notes" />
      <button type="submit">Submit Order</button>
    </form>
  );
}
使用 RHF 的受控组件
import { Controller, useForm } from 'react-hook-form';
import { DatePicker } from '@/components/date-picker';

function EventForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="date"
        control={control}
        rules={{ required: 'Date is required' }}
        render={({ field, fieldState }) => (
          <div>
            <DatePicker
              value={field.value}
              onChange={field.onChange}
            />
            {fieldState.error && (
              <span className="error">{fieldState.error.message}</span>
            )}
          </div>
        )}
      />

      <button type="submit">Create Event</button>
    </form>
  );
}

最佳实践

应做不应做
复杂表单使用 React Hook Form手动管理所有表单状态
使用 Zod/Yup 进行验证从头编写验证逻辑
显示行内错误一次性显示所有错误
使用 aria-* 属性忽略无障碍功能
对开销大的验证进行防抖每次按键都执行异步验证
  • ✅ 需要实时验证时使用受控输入
  • ✅ 使用非受控输入(RHF)以获得更好的性能
  • ✅ 在失焦时验证,改善用户体验
  • ✅ 提交期间显示加载状态
  • ❌ 不要在表单为空时禁用提交按钮
  • ❌ 出错时不要清空表单
  • ❌ 用户交互前不要显示错误

何时不应使用此技能

  • 使用 Zod 模式的 React Hook Form - 使用 react-hook-form 技能了解详细模式
  • React 19 Server Actions 表单 - 使用 react-19 技能了解 useActionState 和 useFormStatus
  • 简单的输入处理 - 使用 react 技能了解基本的受控/非受控输入
  • RHF 之外的表单库 - 遵循相应库的专属文档

反模式

反模式问题解决方案
手动管理所有表单状态样板代码多、容易出错使用 React Hook Form
不验证用户输入安全性/用户体验问题使用 Zod 验证模式
挂载时显示所有错误用户体验差在失焦或提交时验证
表单为空时禁用提交阻止显示验证消息允许提交并显示错误
验证错误时清空表单用户会丢失数据保留表单数据并显示错误
不显示加载状态用户体验令人困惑提交期间显示加载状态
缺少无障碍属性无法访问添加 aria 标签和角色
每次按键都执行异步验证性能问题对异步验证进行防抖

快速故障排除

问题可能原因修复方法
表单未提交缺少 onSubmit 或 action为表单添加 onSubmit 处理函数
验证不起作用未应用模式检查 useForm 中的 resolver
输入未更新未正确受控确保已设置 value 和 onChange
未显示错误缺少 FormMessage添加 FormMessage 组件
提交按钮始终处于禁用状态isPending 检查错误使用 formState.isSubmitting
值未重置未调用 reset()成功后调用 form.reset()
异步验证过于频繁未使用防抖对验证函数进行防抖

参考文档

  • React Hook Form
  • Zod
  • MCP:mcp__documentation__fetch_docs → technology:react,topic:forms

评论

支持 Markdown