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.errorsgives 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 easilyregister()connects inputsformState.errorsshows errorsisSubmittingshows loadingsetError()for server-side errorswatch()andreset()for real-time interactionZod +
zodResolver()= clean validation




