Form Management & Validation Patterns
Production-ready form handling with React Hook Form, Zod validation, complex form patterns, multi-step forms, dynamic forms, and form state management. Learn patterns used in enterprise applications.
๐ 1. React Hook Form Patterns
โก Tired of forms that re-render on every keystroke? React Hook Form is your best friend โ performant, flexible, and so lightweight it's almost unfair! Build forms that don't make your browser cry. ๐๐๏ธ
๐ด Impact: CRITICAL โ Forms are everywhere in web apps โ get them right and your whole app feels snappier!
๐ In this section: register & handleSubmit โข Form validation rules โข Error display โข Custom validation functions
// โ Basic React Hook Formimport { useForm } from 'react-hook-form';interface FormData {email: string;password: string;age: number;}function LoginForm() {const {register,handleSubmit,formState: { errors, isSubmitting }} = useForm<FormData>();const onSubmit = async (data: FormData) => {await login(data);};return (<form onSubmit={handleSubmit(onSubmit)}><div><label htmlFor="email">Email</label><inputid="email"type="email"{...register('email', {required: 'Email is required',pattern: {value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,message: 'Invalid email address'}})}/>{errors.email && <span>{errors.email.message}</span>}</div><div><label htmlFor="password">Password</label><inputid="password"type="password"{...register('password', {required: 'Password is required',minLength: {value: 8,message: 'Password must be at least 8 characters'}})}/>{errors.password && <span>{errors.password.message}</span>}</div><button type="submit" disabled={isSubmitting}>{isSubmitting ? 'Logging in...' : 'Login'}</button></form>);}// โ Advanced: Custom validationfunction AdvancedForm() {const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>();const password = watch('password');const validatePasswordMatch = (value: string) => {return value === password || 'Passwords do not match';};return (<form onSubmit={handleSubmit(onSubmit)}><input{...register('password', {required: 'Password required',validate: {hasUpperCase: (v) =>/[A-Z]/.test(v) || 'Must contain uppercase',hasNumber: (v) =>/[0-9]/.test(v) || 'Must contain number'}})}/><input{...register('confirmPassword', {validate: validatePasswordMatch})}/></form>);}
๐ 2. Zod Schema Validation
๐ฏ Why write validation logic twice (once in TS types, once in validation rules) when Zod does both? Define your schema once, get TypeScript types AND runtime validation for free! It's like a two-for-one deal that actually works. ๐คโจ
๐ Impact: HIGH โ Type-safe validation eliminates an entire category of runtime bugs before they reach production!
๐ In this section: Zod schema definition โข zodResolver integration โข Type inference with z.infer โข Nested object validation
// โ Zod + React Hook Formimport { zodResolver } from '@hookform/resolvers/zod';import { z } from 'zod';import { useForm } from 'react-hook-form';const userSchema = z.object({email: z.string().email('Invalid email'),password: z.string().min(8, 'Password too short'),age: z.number().min(18, 'Must be 18+').max(120),website: z.string().url().optional().or(z.literal('')),tags: z.array(z.string()).min(1, 'At least one tag required')});type UserFormData = z.infer<typeof userSchema>;function UserForm() {const {register,handleSubmit,formState: { errors }} = useForm<UserFormData>({resolver: zodResolver(userSchema)});return (<form onSubmit={handleSubmit(onSubmit)}><input {...register('email')} />{errors.email && <span>{errors.email.message}</span>}<input {...register('password', { valueAsNumber: false })} />{errors.password && <span>{errors.password.message}</span>}<inputtype="number"{...register('age', { valueAsNumber: true })}/>{errors.age && <span>{errors.age.message}</span>}</form>);}// โ Complex nested validationconst addressSchema = z.object({street: z.string().min(1),city: z.string().min(1),zipCode: z.string().regex(/^\d{5}$/, 'Invalid ZIP code')});const orderSchema = z.object({items: z.array(z.object({productId: z.string(),quantity: z.number().min(1),price: z.number().positive()})).min(1),shippingAddress: addressSchema,billingAddress: addressSchema.optional(),useBillingForShipping: z.boolean()}).refine((data) => data.billingAddress || !data.useBillingForShipping,{message: 'Billing address required',path: ['billingAddress']});
๐ช 3. Multi-step Forms
๐ง Giant forms scare users away! Break them into bite-sized steps and suddenly that 20-field form feels like a breezy wizard. Validate at each step, navigate back and forth, and keep all data in one place with FormProvider. Nobody's abandoning THIS checkout flow! ๐โจ
๐ Impact: HIGH โ Multi-step forms dramatically improve completion rates for complex workflows like checkout and onboarding!
๐ In this section: Step-based form architecture โข Per-step validation with trigger โข FormProvider context โข Step indicator UI
// โ Multi-step form with React Hook Formimport { useForm, FormProvider } from 'react-hook-form';const steps = [{ id: 'personal', title: 'Personal Info' },{ id: 'address', title: 'Address' },{ id: 'payment', title: 'Payment' }];function MultiStepForm() {const [currentStep, setCurrentStep] = useState(0);const methods = useForm({mode: 'onChange',defaultValues: {personal: { name: '', email: '' },address: { street: '', city: '' },payment: { card: '', cvv: '' }}});const { trigger, getValues } = methods;const nextStep = async () => {const stepFields = {personal: ['personal.name', 'personal.email'],address: ['address.street', 'address.city'],payment: ['payment.card', 'payment.cvv']};const isValid = await trigger(stepFields[steps[currentStep].id] as any);if (isValid) {setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));}};const prevStep = () => {setCurrentStep(prev => Math.max(prev - 1, 0));};const onSubmit = (data: any) => {console.log('Form data:', data);};return (<FormProvider {...methods}><form onSubmit={methods.handleSubmit(onSubmit)}><StepIndicator currentStep={currentStep} steps={steps} />{currentStep === 0 && <PersonalInfoStep />}{currentStep === 1 && <AddressStep />}{currentStep === 2 && <PaymentStep />}<div>{currentStep > 0 && (<button type="button" onClick={prevStep}>Previous</button>)}{currentStep < steps.length - 1 ? (<button type="button" onClick={nextStep}>Next</button>) : (<button type="submit">Submit</button>)}</div></form></FormProvider>);}function PersonalInfoStep() {const { register, formState: { errors } } = useFormContext();return (<div><input {...register('personal.name', { required: true })} /><input {...register('personal.email', { required: true })} /></div>);}
๐ 4. Dynamic Forms
๐ญ Sometimes you don't know how many fields you'll need โ and that's okay! With useFieldArray, users can add and remove fields on the fly. Think "Add another team member" or "Add another address." Dynamic forms that just work! โโ
๐ต Impact: MEDIUM โ Dynamic forms unlock flexible UIs for invoices, team management, and any repeatable data entry!
๐ In this section: useFieldArray hook โข Dynamic append & remove โข Conditional field rendering โข watch for reactive fields
// โ Dynamic field arraysimport { useFieldArray, useForm } from 'react-hook-form';interface FormData {users: Array<{ name: string; email: string }>;}function DynamicUserForm() {const { control, register, handleSubmit } = useForm<FormData>({defaultValues: {users: [{ name: '', email: '' }]}});const { fields, append, remove } = useFieldArray({control,name: 'users'});return (<form onSubmit={handleSubmit(onSubmit)}>{fields.map((field, index) => (<div key={field.id}><input{...register(`users.${index}.name` as const, {required: 'Name required'})}placeholder="Name"/><input{...register(`users.${index}.email` as const, {required: 'Email required'})}placeholder="Email"/><button type="button" onClick={() => remove(index)}>Remove</button></div>))}<buttontype="button"onClick={() => append({ name: '', email: '' })}>Add User</button><button type="submit">Submit</button></form>);}// โ Conditional fieldsfunction ConditionalForm() {const { watch, register } = useForm();const hasAddress = watch('hasAddress');return (<form><label><input type="checkbox" {...register('hasAddress')} />Has address</label>{hasAddress && (<><input {...register('street')} placeholder="Street" /><input {...register('city')} placeholder="City" /></>)}</form>);}