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 easilyregister()
connects inputsformState.errors
shows errorsisSubmitting
shows loadingsetError()
for server-side errorswatch()
andreset()
for real-time interactionZod +
zodResolver()
= clean validation
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 ๐๐ถ๐ณ๐ฒ๐๐๐๐น๐ฒ.