Skip to content
albarmox logo
albarmox
software engineer · frontend
Back to BlogsJune 1, 2026

Form Handling Done Right: Validation, UX, and Error Feedback

A practical guide to building production-quality forms with React Hook Form and Zod — covering validation, loading states, error messages, and accessibility.

Form Handling Done Right: Validation, UX, and Error Feedback

Forms are one of the most important UI surfaces in any application — and one of the most commonly done wrong. Poor forms frustrate users, create bugs, and erode trust. Good forms are fast, informative, and forgiving. ## The Anatomy of a Good Form A production-quality form needs all of these: - **Default values** — Pre-populate fields when editing existing data - **Validation schema** — Clear, typed rules for what constitutes valid input - **Error messages** — Specific, human-readable feedback at the field level - **Loading state** — Disable submission during pending requests - **Success feedback** — Confirm the action completed - **Error feedback** — Inform the user if something went wrong server-side ## Schema Validation with Zod Zod gives you a type-safe way to declare your form's shape and validation rules: ```ts import { z } from "zod"; export const userSchema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Please enter a valid email address"), role: z.string().min(1, "Role is required"), }); export type UserFormValues = z.infer<typeof userSchema>; ``` The schema serves double duty: it validates your form AND generates your TypeScript types automatically. ## Connecting to React Hook Form ```tsx import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; export function UserForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<UserFormValues>({ resolver: zodResolver(userSchema), defaultValues: { name: "", email: "", role: "" }, }); const onSubmit = async (values: UserFormValues) => { await createUser(values); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label htmlFor="name">Name</label> <input id="name" {...register("name")} /> {errors.name && <p role="alert">{errors.name.message}</p>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Saving..." : "Save"} </button> </form> ); } ``` ## Error Messages That Help Error messages should be specific and actionable: **Bad:** - "Invalid input" - "Error" - "Failed" **Good:** - "Name is required" - "Please enter a valid email address" - "Password must be at least 8 characters" The goal is for a user to read the error and know exactly what to fix — without needing to guess. ## Confirmation for Destructive Actions Any form action that can't be undone — deleting data, sending a final submission, resetting a password — must include a confirmation step. "Are you sure?" dialogs prevent costly accidents. ## The Submit Button Rule Never allow double-submission. Disable the submit button while a request is in flight: ```tsx <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "Saving..." : "Save"} </Button> ``` This is both a UX best practice and a defense against duplicate database entries.

Localoka project preview