Form Architecture

WI-CARPOOL uses React Hook Form with Zod validation for robust form handling with excellent performance and developer experience.

📋 React Hook Form

Performant forms with minimal re-renders and easy validation.

🔍 Zod Validation

Type-safe schema validation with excellent TypeScript integration.

🎨 Shadcn Form Components

Pre-built form components with consistent styling and behavior.

♿ Accessibility

ARIA labels, error announcements, and keyboard navigation support.

Basic Form Setup

Form Component Structure

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

const formSchema = z.object({
  email: z.string().email("Invalid email address"),
  password: z.string().min(8, "Password must be at least 8 characters"),
})

type FormData = z.infer<typeof formSchema>

function LoginForm() {
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  })

  const onSubmit = (data: FormData) => {
    console.log(data)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="Enter your email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" placeholder="Enter password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" className="w-full">
          Sign In
        </Button>
      </form>
    </Form>
  )
}

Advanced Form Examples

Ride Search Form

Complex form with location autocomplete and date/time selection.

SearchForm Implementation

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { PlacesAutocomplete } from "@/components/PlacesAutocomplete"

const searchSchema = z.object({
  from: z.string().min(1, "Starting location is required"),
  to: z.string().min(1, "Destination is required"),
  date: z.date({
    required_error: "Please select a date",
  }),
  passengers: z.number().min(1).max(8),
})

type SearchData = z.infer<typeof searchSchema>

function RideSearchForm() {
  const form = useForm<SearchData>({
    resolver: zodResolver(searchSchema),
    defaultValues: {
      from: "",
      to: "",
      date: new Date(),
      passengers: 1,
    },
  })

  const onSubmit = (data: SearchData) => {
    // Handle search logic
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <FormField
            control={form.control}
            name="from"
            render={({ field }) => (
              <FormItem>
                <FormLabel>From</FormLabel>
                <FormControl>
                  <PlacesAutocomplete
                    placeholder="Enter starting location"
                    value={field.value}
                    onSelect={field.onChange}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          
          <FormField
            control={form.control}
            name="to"
            render={({ field }) => (
              <FormItem>
                <FormLabel>To</FormLabel>
                <FormControl>
                  <PlacesAutocomplete
                    placeholder="Enter destination"
                    value={field.value}
                    onSelect={field.onChange}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>

        <FormField
          control={form.control}
          name="date"
          render={({ field }) => (
            <FormItem className="flex flex-col">
              <FormLabel>Date</FormLabel>
              <Popover>
                <PopoverTrigger asChild>
                  <FormControl>
                    <Button
                      variant="outline"
                      className="w-full pl-3 text-left font-normal"
                    >
                      {field.value ? (
                        format(field.value, "PPP")
                      ) : (
                        <span>Pick a date</span>
                      )}
                      <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                    </Button>
                  </FormControl>
                </PopoverTrigger>
                <PopoverContent className="w-auto p-0" align="start">
                  <Calendar
                    mode="single"
                    selected={field.value}
                    onSelect={field.onChange}
                    disabled={(date) => date < new Date()}
                    initialFocus
                  />
                </PopoverContent>
              </Popover>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" className="w-full">
          Search Rides
        </Button>
      </form>
    </Form>
  )
}

Form Validation Patterns

Common Validation Schemas

User Registration Schema

import { z } from "zod"

const userRegistrationSchema = z.object({
  firstName: z
    .string()
    .min(2, "First name must be at least 2 characters")
    .max(50, "First name must be less than 50 characters"),
  
  lastName: z
    .string()
    .min(2, "Last name must be at least 2 characters")
    .max(50, "Last name must be less than 50 characters"),
  
  email: z
    .string()
    .email("Please enter a valid email address"),
  
  phone: z
    .string()
    .regex(/^\+[1-9]\d{1,14}$/, "Please enter a valid phone number"),
  
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .regex(/(?=.*[a-z])/, "Password must contain at least one lowercase letter")
    .regex(/(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
    .regex(/(?=.*\d)/, "Password must contain at least one number"),
  
  confirmPassword: z.string(),
  
  dateOfBirth: z
    .date()
    .refine((date) => {
      const age = new Date().getFullYear() - date.getFullYear()
      return age >= 18
    }, "You must be at least 18 years old"),
  
  acceptTerms: z
    .boolean()
    .refine((val) => val === true, "You must accept the terms and conditions"),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
})

Vehicle Registration Schema

Vehicle Form Validation

const vehicleSchema = z.object({
  make: z.string().min(1, "Vehicle make is required"),
  model: z.string().min(1, "Vehicle model is required"),
  year: z
    .number()
    .min(2000, "Vehicle must be from 2000 or later")
    .max(new Date().getFullYear() + 1, "Invalid year"),
  
  licensePlate: z
    .string()
    .min(1, "License plate is required")
    .regex(/^[A-Z0-9-\s]+$/i, "Invalid license plate format"),
  
  color: z.string().min(1, "Vehicle color is required"),
  
  seats: z
    .number()
    .min(2, "Vehicle must have at least 2 seats")
    .max(9, "Vehicle cannot have more than 9 seats"),
  
  fuelType: z.enum(["gasoline", "diesel", "electric", "hybrid"], {
    required_error: "Please select a fuel type",
  }),
  
  features: z.array(z.string()).optional(),
  
  images: z
    .array(z.instanceof(File))
    .min(1, "At least one image is required")
    .max(5, "Maximum 5 images allowed"),
})

Form Components

Custom Form Fields

Phone Number Input

import { useState } from "react"
import { CountryCodePicker } from "@/components/CountryCodePicker"
import { Input } from "@/components/ui/input"

interface PhoneInputProps {
  value: string
  onChange: (value: string) => void
  placeholder?: string
}

function PhoneInput({ value, onChange, placeholder }: PhoneInputProps) {
  const [countryCode, setCountryCode] = useState("+1")
  const [phoneNumber, setPhoneNumber] = useState("")

  const handlePhoneChange = (phone: string) => {
    setPhoneNumber(phone)
    onChange(`${countryCode}${phone}`)
  }

  return (
    <div className="flex">
      <CountryCodePicker
        value={countryCode}
        onChange={setCountryCode}
      />
      <Input
        type="tel"
        placeholder={placeholder}
        value={phoneNumber}
        onChange={(e) => handlePhoneChange(e.target.value)}
        className="flex-1"
      />
    </div>
  )
}

File Upload Field

Image Upload Component

import { useCallback } from "react"
import { useDropzone } from "react-dropzone"
import { Upload, X } from "lucide-react"
import { Button } from "@/components/ui/button"

interface FileUploadProps {
  files: File[]
  onFilesChange: (files: File[]) => void
  maxFiles?: number
  accept?: Record<string, string[]>
}

function FileUpload({ files, onFilesChange, maxFiles = 5, accept }: FileUploadProps) {
  const onDrop = useCallback((acceptedFiles: File[]) => {
    const newFiles = [...files, ...acceptedFiles].slice(0, maxFiles)
    onFilesChange(newFiles)
  }, [files, onFilesChange, maxFiles])

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept,
    maxFiles: maxFiles - files.length,
  })

  const removeFile = (index: number) => {
    const newFiles = files.filter((_, i) => i !== index)
    onFilesChange(newFiles)
  }

  return (
    <div className="space-y-4">
      <div
        {...getRootProps()}
        className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
          isDragActive
            ? "border-primary bg-primary/10"
            : "border-gray-300 hover:border-primary"
        }`}
      >
        <input {...getInputProps()} />
        <Upload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
        <p className="text-sm text-gray-600">
          {isDragActive
            ? "Drop the files here..."
            : "Drag 'n' drop files here, or click to select"}
        </p>
      </div>

      {files.length > 0 && (
        <div className="space-y-2">
          {files.map((file, index) => (
            <div
              key={index}
              className="flex items-center justify-between p-2 bg-gray-50 rounded"
            >
              <span className="text-sm truncate">{file.name}</span>
              <Button
                type="button"
                variant="ghost"
                size="sm"
                onClick={() => removeFile(index)}
              >
                <X className="h-4 w-4" />
              </Button>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

Form State Management

Multi-Step Forms

Managing complex forms with multiple steps and state persistence.

Multi-Step Form Hook

import { useState } from "react"

function useMultiStepForm<T>(initialData: T) {
  const [currentStep, setCurrentStep] = useState(0)
  const [formData, setFormData] = useState(initialData)

  const nextStep = () => setCurrentStep((prev) => prev + 1)
  const prevStep = () => setCurrentStep((prev) => Math.max(0, prev - 1))
  const goToStep = (step: number) => setCurrentStep(step)
  
  const updateFormData = (data: Partial<T>) => {
    setFormData((prev) => ({ ...prev, ...data }))
  }

  const resetForm = () => {
    setCurrentStep(0)
    setFormData(initialData)
  }

  return {
    currentStep,
    formData,
    nextStep,
    prevStep,
    goToStep,
    updateFormData,
    resetForm,
  }
}

Form Persistence

Saving form data to localStorage for recovery.

Form Persistence Hook

import { useEffect } from "react"
import { UseFormReturn } from "react-hook-form"

function useFormPersistence<T>(
  form: UseFormReturn<T>,
  key: string,
  exclude: (keyof T)[] = []
) {
  // Save form data to localStorage
  useEffect(() => {
    const subscription = form.watch((data) => {
      const dataToSave = { ...data }
      exclude.forEach((field) => {
        delete dataToSave[field]
      })
      localStorage.setItem(key, JSON.stringify(dataToSave))
    })

    return () => subscription.unsubscribe()
  }, [form, key, exclude])

  // Load form data from localStorage
  useEffect(() => {
    const savedData = localStorage.getItem(key)
    if (savedData) {
      try {
        const parsedData = JSON.parse(savedData)
        Object.keys(parsedData).forEach((field) => {
          if (!exclude.includes(field as keyof T)) {
            form.setValue(field as keyof T, parsedData[field])
          }
        })
      } catch (error) {
        console.error("Failed to load saved form data:", error)
      }
    }
  }, [form, key, exclude])

  const clearSavedData = () => {
    localStorage.removeItem(key)
  }

  return { clearSavedData }
}

Best Practices

Performance Optimization

  • Use React Hook Form's uncontrolled components when possible
  • Debounce expensive validation operations
  • Implement field-level validation for better UX
  • Use form context for deeply nested form components

User Experience

  • Provide clear, actionable error messages
  • Show validation status in real-time
  • Use proper loading states during submission
  • Implement autosave for long forms

Accessibility

  • Associate labels with form controls
  • Use ARIA attributes for complex form interactions
  • Provide clear focus indicators
  • Announce validation errors to screen readers