Welcome back! So far, you have learned how to fetch and display a user’s reading shelf, update progress optimistically, and edit shelf status. Now, let’s focus on making your forms more reliable and user-friendly by adding client-side validation.
Client-side validation helps catch mistakes before data is sent to the server. This means users get instant feedback if they enter something wrong, like leaving a field empty or typing a page number that’s too high. In this lesson, you will learn how to use two popular libraries — Zod and React Hook Form — to add strong, easy-to-understand validation to your forms.
By the end of this lesson, you will know how to:
- Define validation rules for your forms using Zod.
- Connect those rules to your forms with React Hook Form.
- Show clear error messages to users when something is wrong.
Let’s get started!
The central tool in React Hook Form is the useForm hook. It sets up your form state and provides several helper functions:
register: connects each input field to the form state. It simplifies form validation by allowing you to define validation rules directly within the register call, based on HTML standard validation attributes (e.g.,required,minLength,maxLength,pattern).handleSubmit: wraps your submit handler so it only runs if validation passes.formState.errors: stores validation errors for each field.reset,setError, and others provide utilities for resetting values or adding custom errors.
Here’s the simplest possible usage:
In this snippet:
register("username")wires the input into the form’s state system.handleSubmitensures your handler runs with validated data.
React Hook Form revolves around a few central concepts. While the basics are straightforward, understanding the advanced behavior of these tools helps you build scalable, complex forms without unnecessary re-renders or boilerplate.
1. useForm
The useForm hook is the backbone of React Hook Form. It creates and manages the entire lifecycle of your form: tracking values, handling validation, and exposing utilities.
Key options you can configure when calling useForm include:
defaultValues: set initial values for your form fields.mode: determines when validation runs (e.g.,"onChange","onBlur","onSubmit").resolver: allows you to plug in external validation libraries like Zod or Yup.reValidateMode: controls when a field should re-run validation after the first failure.
This makes useForm highly customizable depending on whether you want instant feedback or only final validation on submit.
In this example:
- Fields start with predefined values ( and ).
Zod is a TypeScript-first schema validation library. Instead of scattering validation logic across your app, Zod lets you define a single schema that describes valid input. A schema is a blueprint: it says “this field must be a number,” “this string must have at least 6 characters,” and so on.
Why Zod is preferred:
- Type inference: the TypeScript types for your form are generated automatically from the schema (
z.infer<typeof schema>). - Clear error messages: each validation rule can specify a custom message.
- Composability: schemas can be nested, refined, or reused across forms.
- Consistency: one source of truth for what valid data looks like, used on both client and server if needed.
Here’s a simple schema:
In this schema:
currentPagemust be a number.- It must be an integer (no decimals).
- It cannot be negative.
If a user enters a value that doesn’t match these rules, Zod will return an error message. This makes it easy to keep your data clean and your users informed.
To connect Zod schemas to React Hook Form, we use the zodResolver helper. This adapter allows useForm to run Zod validation whenever the form is submitted or inputs change.
Here’s how you connect them:
Here’s what happens:
resolver: zodResolver(schema)tells React Hook Form to validate with Zod.- If the data doesn’t match the schema, the
errorsobject will contain the relevant messages. - Your submit handler will only run if all validations pass.
This setup keeps all rules in one place and enforces them consistently.
Instead of sprinkling error <p> tags across the app, we use a reusable FormError component to show messages in a consistent style.
- If a
messageprop is provided, it renders in a styled<p>tag. - If no message is passed, it returns
null(nothing is displayed). - This keeps form layouts clean and avoids repeating markup.
You can use this component under any form field to display validation errors. For example:
This will render the error for the currentPage field when validation fails.
Let’s put it all together in the ProgressEditor component. This combines optimistic updates with Zod validation so users can’t enter nonsense values like negative pages or pages beyond the book’s total.
Let’s break down what’s happening:
- The schema uses
z.coerce.number()so even string inputs like"12"are converted to numbers. - It enforces integer values, disallows negatives, and uses
refineto check against the book’s total pages. - React Hook Form is configured with
zodResolver(schema)andcontext: { totalPages }so the validation has access to book-specific data. - If a user enters an invalid value,
errors.currentPage?.messageprovides the error text to theFormErrorcomponent.
This guarantees that only valid page numbers reach the backend mutation.
In this lesson, you learned:
- How
useFormmanages input state, submission, and errors. - What Zod is, how to define schemas with it, and why it’s preferred for clear, type-safe validation.
- How
zodResolverconnects Zod and React Hook Form. - How to display friendly messages with a reusable
FormErrorcomponent.
By combining React Hook Form and Zod, your forms become easier to maintain, safer against invalid input, and friendlier for users. Next, you’ll practice adding validation rules to your own forms and refining error messages to guide users effectively.
