📘 ReactJS

Forms & Validation — React Hook Form + Validation

React Hook Form is a popular form-management library designed to simplify form state, validation, submission, and field registration.

S

Site Administrator

📅 Sep 28, 2026 🕒 6 min read 👁 25 views
☆ Save 𝕏 in f ✆
Forms & Validation — React Hook Form + Validation

Forms become complex very quickly in enterprise applications. A real form can contain dozens of fields, conditional sections, dependent fields, client validation, server validation, asynchronous checks, file uploads, default values, edit mode, submission state, and business rules. This chapter builds a realistic employee-registration form using React Hook Form and demonstrates how the UI behaves.

Why manual form state becomes difficult

With a small form, separate useState variables are manageable. With twenty or thirty fields, manually creating state, change handlers, touched state, validation messages, and submit logic becomes repetitive and error-prone.

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [department, setDepartment] = useState("");
const [salary, setSalary] = useState(0);
// ...many more fields

Install React Hook Form

npm install react-hook-form

For schema validation, a common option is Zod with a resolver.

npm install zod @hookform/resolvers

Complete employee form

import { useForm } from "react-hook-form";

function EmployeeForm() {
  const {
    register,
    handleSubmit,
    formState: {
      errors,
      isSubmitting,
      isDirty,
      isValid
    }
  } = useForm({
    mode: "onChange",
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
      department: "",
      experience: 0
    }
  });

  async function onSubmit(data) {
    console.log(data);
    await saveEmployee(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("firstName", {
          required: "First name is required"
        })}
      />

      {errors.firstName && (
        <p>{errors.firstName.message}</p>
      )}

      <input
        {...register("email", {
          required: "Email is required",
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: "Invalid email"
          }
        })}
      />

      {errors.email && (
        <p>{errors.email.message}</p>
      )}

      <button
        disabled={isSubmitting || !isValid}
      >
        {isSubmitting ? "Saving..." : "Save Employee"}
      </button>
    </form>
  );
}

What register actually does

register connects the input to React Hook Form. It supplies the event handlers, field name, and reference information required by the form library.

const registration = register("email");

console.log(registration);

// Conceptually contains:
// name
// onChange
// onBlur
// ref

Validation rules

Rules can be attached to individual fields.

register("experience", {
  required: "Experience is required",
  min: {
    value: 0,
    message: "Cannot be negative"
  },
  max: {
    value: 50,
    message: "Enter a realistic value"
  },
  valueAsNumber: true
})

Complete validation output

User clicks Save Employee

First name is empty:
First name is required

Email is invalid:
Invalid email

Experience is -2:
Cannot be negative

All fields valid:
Saving...

API succeeds:
Employee created successfully

Touched, dirty, valid, submitting

Real forms need more than a value. React Hook Form exposes useful state such as whether the user changed a field, whether validation has passed, and whether submission is running.

const {
  errors,
  isDirty,
  isValid,
  isSubmitting
} = formState;

Conditional fields

Business forms often have conditional sections. For example, contractors may need a contract-end date while permanent employees do not.

const employmentType = watch("employmentType");

{employmentType === "contract" && (
  <input
    type="date"
    {...register("contractEndDate", {
      required: "Contract end date is required"
    })}
  />
)}

Dependent validation

Some rules depend on another field. For example, a confirm-password field must match the password.

<input
  type="password"
  {...register("password", {
    required: "Password is required",
    minLength: {
      value: 8,
      message: "Minimum 8 characters"
    }
  })}
/>

<input
  type="password"
  {...register("confirmPassword", {
    validate: value =>
      value === watch("password") ||
      "Passwords do not match"
  })}
/>

Schema validation with Zod

import { z } from "zod";

const employeeSchema = z.object({
  firstName: z
    .string()
    .min(1, "First name is required"),

  lastName: z
    .string()
    .min(1, "Last name is required"),

  email: z
    .string()
    .email("Invalid email"),

  experience: z
    .number()
    .min(0)
    .max(50)
});

Connect the schema

import { zodResolver } from "@hookform/resolvers/zod";

const form = useForm({
  resolver: zodResolver(employeeSchema),
  defaultValues: {
    firstName: "",
    lastName: "",
    email: "",
    experience: 0
  }
});

Why schema validation helps

When validation becomes complex, keeping rules in one schema makes them easier to review and test. It can also reduce duplication between related forms. The backend still needs its own validation.

Editing an existing record

Edit screens usually receive data from an API after the form has been created. Use reset to populate the form with the loaded record.

useEffect(() => {
  async function loadEmployee() {
    const employee = await getEmployee(id);

    reset({
      firstName: employee.firstName,
      lastName: employee.lastName,
      email: employee.email,
      experience: employee.experience
    });
  }

  loadEmployee();
}, [id, reset]);

Server-side validation

Client validation cannot know every business rule. For example, an email may have a valid format but already belong to another employee. The backend may return field-specific errors.

try {
  await createEmployee(data);
} catch (error) {
  if (error.code === "EMAIL_EXISTS") {
    setError("email", {
      type: "server",
      message: "Email is already registered"
    });
  }
}

Complete form submission flow

flowchart TD
A[User enters data] --> B[React Hook Form]
B --> C[Client validation]
C --> D{Valid}
D -->|No| E[Show field errors]
D -->|Yes| F[Submit API request]
F --> G{Server result}
G -->|Validation error| H[Set field errors]
G -->|Success| I[Show success]
G -->|Failure| J[Show general error]

Loading and double-submit prevention

Users can click a submit button more than once. Disable the button while the request is in progress and make the backend operation safe against accidental duplicate submissions where appropriate.

<button disabled={isSubmitting}>
  {isSubmitting ? "Saving..." : "Save"}
</button>

Reset after success

async function onSubmit(data) {
  await createEmployee(data);

  reset();
  showMessage("Employee created successfully");
}

File upload

File inputs can be registered, but uploading files normally requires constructing multipart form data or using an upload API designed for the application's requirements.

const file = watch("resume")?.[0];

const formData = new FormData();
formData.append("resume", file);

await fetch("/api/employees/resume", {
  method: "POST",
  body: formData
});

Large form component design

Do not put a 1,000-line form component into a single file. Break the UI into reusable sections such as PersonalDetails, EmploymentDetails, Compensation, and Documents while keeping one form context where appropriate.

function EmployeeForm() {
  const methods = useForm();

  return (
    <FormProvider {...methods}>
      <PersonalDetails />
      <EmploymentDetails />
      <Compensation />
      <Documents />
    </FormProvider>
  );
}

Common mistakes

  • Using client validation as a security boundary.
  • Not handling server validation.
  • Allowing repeated submission while an API request is active.
  • Hardcoding error display differently for every field.
  • Keeping a huge form in one component.
  • Forgetting to reset edit forms when the record changes.

Interview questions

  • Why React Hook Form?
  • What does register do?
  • How do you handle validation errors?
  • How do you integrate Zod or another schema validator?
  • How do you handle server validation?
  • How do you implement conditional fields?
  • How do you prevent duplicate submissions?

Key takeaways

  • Use a form library when form complexity justifies it.
  • Keep client and server validation separate.
  • Handle field errors, server errors, loading, success, and reset behavior.
  • For large forms, organize UI into reusable sections.

Comments & Ratings (0)

Log in or create a student account to comment and rate this article.

Be the first to comment on this article.

Never miss a new post 📬

Get an email whenever a new article, project, cheatsheet or video drops.

✅

ℹ️