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>
)
}
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.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"
const application = Application.start()
application.debug = false
Disable submit button while Turbo form is submitting
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
url: String,
delay: { type: Number, default: 800 },
Stimulus: autosave draft with Turbo-friendly requests
Share this code
Here's the card — post it anywhere.