Form handling with React Hook Form

Maya Patel Jan 2026
1 tab
import { useForm } from 'react-hook-form'
import { PostFormData } from '@/types'

interface PostFormProps {
  initialData?: Partial<PostFormData>
  onSubmit: (data: PostFormData) => Promise<void>
  isSubmitting?: boolean
}

export function PostForm({ initialData, onSubmit, isSubmitting }: PostFormProps) {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm<PostFormData>({
    defaultValues: initialData,
  })

  const handleFormSubmit = async (data: PostFormData) => {
    try {
      await onSubmit(data)
    } catch (error: any) {
      // Map server errors to form fields
      if (error.data?.errors) {
        Object.entries(error.data.errors).forEach(([field, messages]) => {
          setError(field as keyof PostFormData, {
            message: (messages as string[])[0],
          })
        })
      }
    }
  }

  return (
    <form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
      <div>
        <label htmlFor="title" className="block text-sm font-medium mb-1">
          Title
        </label>
        <input
          {...register('title', {
            required: 'Title is required',
            minLength: { value: 5, message: 'Title must be at least 5 characters' },
            maxLength: { value: 100, message: 'Title must be at most 100 characters' },
          })}
          id="title"
          className="w-full px-3 py-2 border rounded"
        />
        {errors.title && (
          <p className="text-red-600 text-sm mt-1">{errors.title.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="body" className="block text-sm font-medium mb-1">
          Body
        </label>
        <textarea
          {...register('body', {
            required: 'Body is required',
            minLength: { value: 50, message: 'Body must be at least 50 characters' },
          })}
          id="body"
          rows={10}
          className="w-full px-3 py-2 border rounded"
        />
        {errors.body && (
          <p className="text-red-600 text-sm mt-1">{errors.body.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="status" className="block text-sm font-medium mb-1">
          Status
        </label>
        <select {...register('status')} id="status" className="w-full px-3 py-2 border rounded">
          <option value="draft">Draft</option>
          <option value="published">Published</option>
        </select>
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Saving...' : 'Save Post'}
      </button>
    </form>
  )
}
1 file · typescript Explain with highlit

React Hook Form provides performant form handling with minimal re-renders. Unlike controlled components that re-render on every keystroke, it uses uncontrolled inputs with refs. The register function connects inputs to the form state, and handleSubmit wraps the submission handler with validation. Built-in validation rules like required, minLength, and pattern cover common cases, while custom validators handle business logic. Integration with Yup or Zod provides schema-based validation. The formState object exposes errors, touched fields, and submission state. For complex forms, I use watch sparingly to avoid performance issues. Server-side validation errors from Rails are mapped to form fields with setError.