--- name: form-react description: Production-ready React form patterns using React Hook Form (default) and TanStack Form with Zod integration. Use when building forms in React applications. Implements reward-early-punish-late validation timing. --- # Form React Production React form patterns. Default stack: **React Hook Form + Zod**. ## Quick Start ```bash npm install react-hook-form @hookform/resolvers zod ``` ```tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // 1. Define schema const schema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Min 8 characters') }); type FormData = z.infer; // 2. Use form function LoginForm() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), mode: 'onBlur' // Reward early, punish late }); return (
console.log(data))}> {errors.email && {errors.email.message}} {errors.password && {errors.password.message}}
); } ``` ## When to Use Which | Criteria | React Hook Form | TanStack Form | |----------|-----------------|---------------| | Performance | ✅ Best (uncontrolled) | Good (controlled) | | Bundle size | 12KB | ~15KB | | TypeScript | Good | ✅ Excellent | | Cross-framework | ❌ React only | ✅ Multi-framework | | React Native | Requires workarounds | ✅ Native support | | Built-in async validation | Manual | ✅ Built-in debouncing | | Ecosystem | ✅ Mature (4+ years) | Growing | **Default: React Hook Form** — Better performance for most React web apps. **Use TanStack Form when:** - Building cross-framework component libraries - Need strict controlled component behavior - Heavy async validation (username checks) - React Native applications ## React Hook Form Patterns ### Basic Form ```tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { loginSchema, type LoginFormData } from './schemas'; export function LoginForm({ onSubmit }: { onSubmit: (data: LoginFormData) => void }) { const { register, handleSubmit, formState: { errors, isSubmitting, touchedFields } } = useForm({ resolver: zodResolver(loginSchema), mode: 'onBlur', // First validation on blur (punish late) reValidateMode: 'onChange' // Re-validate on change (real-time correction) }); return (
{touchedFields.email && errors.email && ( {errors.email.message} )}
{touchedFields.password && errors.password && ( {errors.password.message} )}
); } ``` ### Reusable Form Field Component ```tsx // FormField.tsx import { useFormContext } from 'react-hook-form'; import { ReactNode } from 'react'; interface FormFieldProps { name: string; label: string; type?: string; autoComplete?: string; hint?: string; required?: boolean; children?: ReactNode; } export function FormField({ name, label, type = 'text', autoComplete, hint, required, children }: FormFieldProps) { const { register, formState: { errors, touchedFields } } = useFormContext(); const error = errors[name]; const touched = touchedFields[name]; const showError = touched && error; const showValid = touched && !error; return (
{hint && {hint}} {children || ( )} {showError && ( {error.message as string} )}
); } ``` ### Using FormProvider for Nested Components ```tsx // Form wrapper import { FormProvider, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; export function CheckoutForm() { const methods = useForm({ resolver: zodResolver(checkoutSchema), mode: 'onBlur' }); return (
); } // Nested section component function ContactSection() { return (
Contact Information
); } ``` ### Watching Values (Real-time) ```tsx import { useForm, useWatch } from 'react-hook-form'; function RegistrationForm() { const { register, control } = useForm(); // Watch password for strength meter const password = useWatch({ control, name: 'password', defaultValue: '' }); return (
); } ``` ### Conditional Fields ```tsx import { useFormContext, useWatch } from 'react-hook-form'; function ConditionalField({ watchField, condition, children }) { const { control } = useFormContext(); const value = useWatch({ control, name: watchField }); if (!condition(value)) return null; return <>{children}; } // Usage val === true}> ``` ### Async Validation (Username Check) ```tsx import { useForm } from 'react-hook-form'; function RegistrationForm() { const { register, setError, clearErrors } = useForm(); const checkUsername = async (username: string) => { if (username.length < 3) return; const response = await fetch(`/api/check-username?u=${username}`); const { available } = await response.json(); if (!available) { setError('username', { type: 'manual', message: 'Username is taken' }); } else { clearErrors('username'); } }; return ( checkUsername(e.target.value)} /> ); } ``` ### Form Reset ```tsx function EditProfileForm({ defaultValues }) { const { reset, handleSubmit } = useForm({ defaultValues }); // Reset to new values useEffect(() => { reset(defaultValues); }, [defaultValues, reset]); // Reset to initial values const handleCancel = () => reset(); return (
{/* fields */}
); } ``` ### Array Fields (Dynamic) ```tsx import { useFieldArray, useForm } from 'react-hook-form'; function TeamMembersForm() { const { control, register } = useForm({ defaultValues: { members: [{ name: '', email: '' }] } }); const { fields, append, remove } = useFieldArray({ control, name: 'members' }); return (
{fields.map((field, index) => (
))}
); } ``` ## TanStack Form Patterns ### Basic Form ```tsx import { useForm } from '@tanstack/react-form'; import { zodValidator } from '@tanstack/zod-form-adapter'; import { loginSchema } from './schemas'; function LoginForm() { const form = useForm({ defaultValues: { email: '', password: '' }, onSubmit: async ({ value }) => { await login(value); }, validatorAdapter: zodValidator() }); return (
{ e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }} > {(field) => (
field.handleChange(e.target.value)} onBlur={field.handleBlur} /> {field.state.meta.isTouched && field.state.meta.errors.length > 0 && ( {field.state.meta.errors[0]} )}
)}
{(field) => (
field.handleChange(e.target.value)} onBlur={field.handleBlur} /> {field.state.meta.isTouched && field.state.meta.errors.length > 0 && ( {field.state.meta.errors[0]} )}
)}
[state.canSubmit, state.isSubmitting]}> {([canSubmit, isSubmitting]) => ( )}
); } ``` ### Async Validation with Debouncing ```tsx { const response = await fetch(`/api/check-username?u=${value}`); const { available } = await response.json(); return available ? undefined : 'Username is taken'; } }} > {(field) => (
field.handleChange(e.target.value)} onBlur={field.handleBlur} /> {field.state.meta.isValidating && Checking...} {field.state.meta.errors[0] && {field.state.meta.errors[0]}}
)}
``` ### Linked Fields (Password Confirmation) ```tsx { const password = fieldApi.form.getFieldValue('password'); return value !== password ? 'Passwords do not match' : undefined; } }} > {(field) => ( field.handleChange(e.target.value)} /> )} ``` ## Common Patterns ### Form with Server Errors ```tsx function LoginForm() { const [serverError, setServerError] = useState(null); const { handleSubmit, setError } = useForm({ resolver: zodResolver(loginSchema) }); const onSubmit = async (data: LoginFormData) => { try { setServerError(null); await login(data); } catch (error) { if (error.field) { // Field-specific error setError(error.field, { message: error.message }); } else { // General error setServerError(error.message); } } }; return (
{serverError && (
{serverError}
)} {/* fields */}
); } ``` ### Loading State ```tsx function ContactForm() { const { handleSubmit, formState: { isSubmitting } } = useForm(); return (
{/* fields */}
); } ``` ### Focus First Error ```tsx import { useForm } from 'react-hook-form'; import { useRef, useEffect } from 'react'; function MyForm() { const formRef = useRef(null); const { handleSubmit, formState: { errors, isSubmitSuccessful } } = useForm(); // Focus first error after failed submit useEffect(() => { if (Object.keys(errors).length > 0) { const firstError = formRef.current?.querySelector('[aria-invalid="true"]'); (firstError as HTMLElement)?.focus(); } }, [errors]); return
{/* fields */}
; } ``` ## File Structure ``` form-react/ ├── SKILL.md ├── references/ │ ├── rhf-patterns.md # React Hook Form deep-dive │ ├── tanstack-patterns.md # TanStack Form deep-dive │ └── migration-guide.md # Formik → RHF migration └── scripts/ ├── rhf-form-builder.tsx # RHF form patterns ├── tanstack-form-builder.tsx # TanStack patterns ├── form-field.tsx # Reusable field component ├── use-form-field.ts # Custom hook └── schemas/ # Shared with form-validation ├── auth.ts ├── profile.ts └── payment.ts ``` ## Integration with Other Skills ```tsx // Combine: form-react + form-validation + form-accessibility + form-security import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { loginSchema } from 'form-validation/schemas/auth'; import { FormField } from 'form-accessibility/aria-form-wrapper'; import { AUTOCOMPLETE } from 'form-security/autocomplete-config'; function LoginForm({ onSubmit }) { const methods = useForm({ resolver: zodResolver(loginSchema), mode: 'onBlur' }); return (
{/* ... */}
); } ``` ## Reference - `references/rhf-patterns.md` — Complete React Hook Form patterns - `references/tanstack-patterns.md` — TanStack Form patterns - `references/migration-guide.md` — Migrating from Formik