Skip to Content
Module 6: Professional Forms6.2 React Hook Form

Module 6.2: React Hook Form

React Hook Form: Streamlining Form Management in React Applications

🎯 Learning Objectives

By the end of this lesson, you’ll master:

  • React Hook Form (RHF): The most popular form management library in the React ecosystem.
  • Why RHF Matters: Solve the excessive re-rendering issue common with traditional Controlled Forms.
  • Core API: Take command of useForm, register, handleSubmit, and formState.
  • Validation: Integrate basic error checking.

1. Why Choose React Hook Form?

In complex forms, using useState for each input leads to re-rendering the entire component with every single keystroke.

React Hook Form tackles this by leveraging Uncontrolled Components (DOM) in conjunction with Refs to manage data, effectively eliminating unnecessary re-renders.

Comparison of React Hook Form performance versus traditional controlled components, showing fewer re-renders.

2. Setup

Open your project directory in the terminal and run:

npm install react-hook-form # hoặc yarn add react-hook-form # hoặc pnpm add react-hook-form # hoặc bun add react-hook-form

3. Core Concepts (Core API)

React Hook Form provides powerful APIs for optimal form handling.

const { register, handleSubmit, watch, formState, reset, setValue, control } = useForm();

3.1. register & Validation Rules

register is the most critical function, used to connect your input fields with RHF. It comes with built-in support for HTML5 validation standards.

<input {...register('firstName', { required: true, maxLength: 20 })} />

| Rule | Description | Example | | :---------------------- | :---------------------------- | :------------------------------------------------------- | --- | -------------- | | required | Input is required | { required: "Cannot be empty" } | | min/max | Minimum/Maximum numeric value | { min: { value: 18, message: "Must be over 18" } } | | minLength/maxLength | String length | { minLength: 6 } | | pattern | Regex validation | { pattern: /^\S+@\S+$/i } | | validate | Custom validation function | { validate: value => value === "admin" | | "Nice try!" } |

3.2. formState Anatomy

formState holds information about the entire form’s state. Important: RHF uses Proxies, so you must destructure or directly access a property for RHF to subscribe to that state (which helps optimize renders).

  • errors: An object containing validation errors.
  • isSubmitting: true when the form is currently submitting (asynchronous).
  • isDirty: true when the user has changed any value compared to defaultValues.
  • isValid: true when there are no validation errors (often used to disable the submit button).
  • dirtyFields: An object tracking specific fields that have been modified.
// This will only re-render when isDirty changes, not when errors change (if errors are not used) const { isDirty, isSubmitting } = formState;

3.3. handleSubmit

This wrapper function automatically prevents e.preventDefault() and collects form data.

const onSubmit = (data) => console.log(data); const onError = (errors) => console.log(errors); <form onSubmit={handleSubmit(onSubmit, onError)} />;

3.4. watch vs getValues

  • watch("email"): Returns the field's value and re-renders the component every time that field changes. Use this for dynamic UI display.
  • getValues("email"): Returns the current value but DOES NOT re-render. Use this in event handlers or logic that doesn’t require UI updates.

5. Advanced APIs & Techniques

5.1. Reset Form (reset)

Use this to clear form data or reset it to new default values (often after a successful submission or fetching data from an API).

const { reset } = useForm(); // Reset to empty reset(); // Reset to new values (keeping formState if needed) reset({ firstName: 'Luan', lastName: 'Thnh' }, { keepDirty: true });

5.2. Manual Data Manipulation (setValue, trigger)

  • setValue("name", "value"): Sets a field's value without user input.
  • trigger("email"): Manually triggers validation for a single field or the entire form.
<button onClick={() => setValue("age", 18)}>Set Age 18</button> <button onClick={() => trigger("email")}>Validate Email Only</button>

5.3. Integrating UI Libraries (Controller)

For complex components that aren’t native HTML inputs (like react-select, MUI DatePicker, AntD Input), we can’t use register (because there’s no ref pointing to a real input). In these cases, we use the <Controller /> component.

import { Controller } from 'react-hook-form'; <Controller name="price" control={control} rules={{ required: true }} render={({ field }) => ( // field contains: onChange, onBlur, value, ref <input {...field} onChange={(e) = /> field.onChange(parseInt(e.target.value))} /> )} />;

5.4. Performance Optimization

  1. Use mode: 'onBlur': Only validate when the user leaves an input field to prevent continuous re-renders during typing (onChange mode).
  2. Destructuring formState Smartly: Only extract what you truly need. For example, if you only need isSubmitting to display a loading indicator, don’t destructure errors if that component isn’t displaying errors.
  3. Limit watch Usage: Using watch excessively in a parent component will trigger re-renders for the entire form. Instead, break down your components and use useWatch or control to isolate re-renders.

5.5. Dynamic Forms with useFieldArray

Handle forms where the number of fields changes dynamically (e.g., adding multiple addresses, multiple family members).

The Challenge: If you use plain useState with an array, you’ll face difficulties managing Key IDs, validating individual fields, and performance (re-rendering the entire list).

The Solution: RHF’s useFieldArray.

import { useForm, useFieldArray } from 'react-hook-form'; const { control, register } = useForm(); const { fields, append, remove } = useFieldArray({ control, name: 'items', // field name in formValues }); return ( <ul> {fields.map((item, index) => ( <li key={item.id}> {/* Important: use item.id as the key */} <input {...register(`items.${index}.name`)} /> <button onClick={() => remove(index)}>Delete</button> </li> ))} <button onClick={() => append({ name: 'New Item' })}>Add</button> </ul> );

6. Hands-on Labs

Lab 1: Flight Booking Widget

Build a flight booking form with a card-style UI (Boarding Pass style).

  • Features:
    • Calculate real-time ticket prices based on seat class (Class) and number of passengers (watch).
    • Input Range Slider for the number of passengers.
    • Custom Radio Buttons for selecting seat class.
  • Techniques: Use watch for logic calculation, register with different input types (range, date, select).

Flight Booking Widget UI with inputs for origin, destination, dates, and passenger details.

💡 Hints:

  • Use watch(["class", "passengers"]) to get real-time values and multiply by the unit price.
  • Use CSS peer to style custom radio buttons.

Lab 2: Profile Settings Dashboard

Create a professional account settings page with complex conditional logic.

  • Features:
    • Sidebar simulating a Dashboard UI.
    • Conditional Field: Only display “Hourly Rate” when the “Available for Hire” toggle is enabled.
    • Character Counter: Display remaining character count for the Bio.
    • Dirty State: Display “Unsaved Changes” state when the user modifies the form.
  • Techniques: watch for conditional rendering, formState.isDirty for UI feedback, Nested Object data (socials.twitter).

Profile Settings Dashboard UI with various input fields, toggles, and character counters.

💡 Hints:

  • register("socials.twitter") for RHF to automatically create a nested object upon submit.
  • Check formState.isDirty to disable the Save button if no changes have been made.

7. Recap

  1. Performance: React Hook Form makes your forms fluid by minimizing re-renders.
  2. Simplicity: The API is easy to use (register, handleSubmit).
  3. Integration: Easily integrate with UI libraries (Material UI, AntD) via Controller (which you’ll learn later).
  4. Validation: Powerful validation support is available directly within the register function.

8. References

Last updated on