React Hook Form Simplified: From Beginner to Expert


๐Ÿš€ What is React Hook Form?

React Hook Form (RHF) is a lightweight library to manage form state in React using hooks. Itโ€™s:

  • Super easy to set up

  • Has built-in validation

  • Works well with schema validation tools like Zod


๐Ÿ”ง Basic Setup

First, install the libraries:

npm install react-hook-form
npm install zod @hookform/resolvers

Now, letโ€™s build a simple login form with validation:

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

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: "Email is required" })} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}

      <input {...register("password", { required: "Password is required" })} type="password" placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

๐Ÿ’ก Explanation:

  • register() connects input fields to React Hook Form.

  • handleSubmit() wraps the submit function.

  • errors gives us access to validation error messages.


โœ… Schema Validation with Zod

Zod is a library that lets you define a validation schema. Letโ€™s use it with RHF for cleaner validation.

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useForm } from "react-hook-form";

const schema = z.object({
  email: z.string().email("Invalid email format"),
  password: z.string().min(6, "Password must be at least 6 characters"),
});

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}

      <input {...register("password")} type="password" placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

๐ŸŽฏ Why use Zod?

  • Keeps validation rules outside of JSX

  • Automatically integrates with TypeScript

  • Makes large forms easier to manage


โš™๏ธ Handling Server Errors & Loading State

Letโ€™s handle a fake server error like โ€œemail already existsโ€ and show a loading button.

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
  setError,
} = useForm({
  resolver: zodResolver(schema),
});

const onSubmit = async (data) => {
  try {
    await new Promise((res) => setTimeout(res, 1000));
    throw new Error(); // simulate server error
  } catch {
    setError("root", { message: "Email already exists!" });
  }
};

๐Ÿ’ก isSubmitting disables the button while waiting.

๐Ÿ’ก setError("root", { message }) is used to show global/server errors.


๐ŸŒŸ Advanced Features You Should Know

Here are some powerful tools I also explored:

1. watch()

Tracks real-time values of inputs.

const watchFields = watch(["email", "password"]);

2. reset()

Resets the form values to initial state or clears it.

reset();

3. Controller

Used for custom components (like dropdowns, sliders).

4. useFieldArray

Used for dynamic fields like adding multiple hobbies or phone numbers.

5. FormProvider

Helps share the form logic across multiple child components.


๐Ÿงช Final Code with Everything Together

Hereโ€™s a full form with Zod, server error, isSubmitting, and real-time validation:

import { useForm, FormProvider } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  email: z.string().email("Enter a valid email"),
  password: z.string().min(6, "Minimum 6 characters"),
});

export default function App() {
  const methods = useForm({
    resolver: zodResolver(schema),
  });

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
    watch,
    reset,
  } = methods;

  const onSubmit = async (data) => {
    try {
      await new Promise((r) => setTimeout(r, 1000));
      throw new Error(); // Simulate error
    } catch {
      setError("root", { message: "Email already exists!" });
    }
  };

  return (
    <FormProvider {...methods}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <h2>Signup Form</h2>
        <input {...register("email")} placeholder="Email" />
        {errors.email && <p>{errors.email.message}</p>}

        <input {...register("password")} placeholder="Password" type="password" />
        {errors.password && <p>{errors.password.message}</p>}

        <button disabled={isSubmitting}>
          {isSubmitting ? "Submitting..." : "Submit"}
        </button>

        {errors.root && <p>{errors.root.message}</p>}
      </form>
    </FormProvider>
  );
}

๐ŸŽ‰ Summary

  • useForm() handles form state easily

  • register() connects inputs

  • formState.errors shows errors

  • isSubmitting shows loading

  • setError() for server-side errors

  • watch() and reset() for real-time interaction

  • Zod + zodResolver() = clean validation

0
Subscribe to my newsletter

Read articles from UR Prakash Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

UR Prakash Gupta
UR Prakash Gupta

My name is ๐”๐‘ ๐๐ซ๐š๐ค๐š๐ฌ๐ก ๐†๐ฎ๐ฉ๐ญ๐š and I talk about ๐—ง๐—ฒ๐—ฐ๐—ต-๐Š๐ง๐จ๐ฐ๐ฅ๐ž๐๐ ๐ž, ๐—ช๐—ฒ๐—ฏ๐——๐—ฒ๐˜ƒ, ๐——๐—ฒ๐˜ƒ๐—ข๐—ฝ๐˜€ and ๐—Ÿ๐—ถ๐—ณ๐—ฒ๐˜€๐˜๐˜†๐—น๐—ฒ.