React Hook Form Basics#

This guide demonstrates the basics of using React Hook Form in Next.js. We cover the most fundamental RHF APIs for implementing performant React forms with intuitive client side field validations and form submission.

👉 New to App-Generator? SignIN with GitHub or Download a PRO Starter for only $19.99/mo

Pre-requisites#

Next.js & TypeScript#

This guide assumes you’re hands on with Next.js and are pretty wearied of building complex forms with controlled React fields. Also, you are keen on building nuanced React forms with TypeScript.

Next.js App with React Hook Form#

First, start a Next.js app named nextjs-rhf with app router, TypeScript and TailwindCSS. So, in your terminal execute:

npx create-next-app@latest

And choose a configuration similar to the following:

✔ What is your project named? … nextjs-rhf
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for next dev? … No
✔ Would you like to customize the import alias (@/* by default)? … Yes
✔ What import alias would you like configured? … @/*

Once initialized, run the application with:

npm run dev

This should have the app live on: http://localhost:3000/

Set Up React hook Form#

React Hook Form is a robust React form management library that helps build nuanced form experiences, from simple forms to advanced multi-step forms that demand integration to third party libraries.

There’s only one package for using all native features of React Hook Form. It already comes with supported TS types, so no DefinitelyTyped packages are needed.

To install React Hook Form, run:

npm i react-hook-form

This sets us up for implementing neat React forms by managing a form’s data, its overall state, errors, individual fields data, their states, validations, and much more – using just one hook, the useForm() hook.

Additional Packages#

The example app built in this guide uses TailwindCSS with DaisyUI. It also uses a JSON Server to store and query data via REST endpoints. Feel free to refer to their respective docs in case you need to dive into their setup.

Working with React Hook Form in Next.js#

Inside a React component, we have to initialize a React Hook Form instance with the useForm() hook import-ed from the react-hook-form package.

const {
  register,
  handleSubmit,
  watch,
  getValues,
  formState: { errors, isDirty, isValid },
} = useForm<TPost>({
      mode: "onChange",
      criteriaMode: "all",
      reValidateMode: "onSubmit",
    });

Doing so gives a local RHF object with a bunch of methods and objects to construct the form according to our needs. The most essential React Hook Form APIs include register, formState, handleSubmit and reset. formState encapsulates different states the form can be in. For this demo, we’re interested in the errors, isDirty and isValid properties.

With these fundamental RHF APIs, we can build some decent form features with responsive field validations and error feedback that are superior than those painfully built from scratch.

Configuring React Hook Form#

We pass a configuration object to useForm() in order to prefer certain form behaviors. For example:

const form = useForm<TPost>({
  mode: "onChange",
  defaultValues: {
    title: "",
    subtitle: "",
    content: "",
  },
  criteriaMode: "all",
  reValidateMode: "onSubmit",
});

Here, mode lets us choose when field validations should run, and we want them to run on the "onChange" event. With reValidateMode: "onSubmit", we want to run a revalidation when form submission is invoked. And with criteriaMode: "all", we want access to all errors on a particular field. We have also set default values of the fields with the defaultValues property.

Other behaviors we can set are values, shouldFocusError, resetOptions, shouldUnregister, etc. For details, please refer to the React Hook Form configuration docs here.

Form Data & Field Registration#

Form data in React Hook Form is central to all its features. RHF form data is automatically availed to the handleSubmit() method for submission and is generally inferred as data. It can be accessed by getValues() as well as via watch().

Form data is a null-ish object at initialization. We have to explicitly add fields to it with the register() method. As with the title field shown below:

<input
  {...register("title")}
  type="text"
  placeholder="Add a title"
/>

The register() method produces React Hook Form specific values for name, ref, onChange and onBlur properties for us to place on a target form field. So, with the above {...register("title")} spread operation, we are registering title to the form data, and setting React Hook Form values for name, ref, onChange and onBlur on the <input /> field.

That way, title is now registered as a field on the form data object. And the <input /> field inherits the behavior of a React Hook Form input field.

Setting Validation Rules#

We set up validation rules while registering a field. For example, for the title field, we have two rules:

<input
        {...register("title", {
                minLength: {
                        value: 3,
                        message: "Title should be at least 3 characters."
                },
                maxLength: {
                        value: 60,
                        message: "Title exceeded the limit of 60 characters."
                },
        })}
/>

Here, we have defined maxLength and minLength rules with custom messages using React Hook Form’s long format. For shorter validation formats, feel free to check this section of RHF docs.

Displaying Errors in React Hook Form#

React Hook Form gives us the formState object that keeps track of various states of the form. States stored in formState include errors and other details related to whether the form or a particular field has been touched, changed; or the form is loading, submitting or submitted, etc.

We can destructure the formState.errors object and display validation feedbacks while a specific field gets filled. For example, for the title <input /> field, we can display its error in a <span /> tag:

{ errors?.title && <span className="my-2 text-xs text-red-700">{errors?.title?.message}</span> }

Submitting a Form in RHF#

For submitting a form, we pass the handleSubmit() method to onSubmit event on the <form/> element:

<form
  onSubmit={handleSubmit(createNewPost)}
>
...
</form>

The handleSubmit() method accepts a form submission handler and hands over the form’s data to it. For this form, we have a createNewPost() function that creates a post from the form data. It looks like this:

const createNewPost = async (data: TPost) => {
  await createPost(data);

  reset(defaultValues);
  redirect("/");
};

The submit handler (createNewPost here), typically uses data to performs database operations. In our case, it invokes a Next.js server action: createPost().

Resetting a Form in RHF#

We can reset the RHF form to a desired value with the React Hook Form reset() API. For example, inside the createNewPost() function, we have reset our form to defaultValues:

reset(defaultValues);

Using Form States in React Hook Form#

React Hook Form makes it extremely convenient to implement superior form experiences. For example, we can apply a button lock when the form is isDirty and is not valid (!isValid):

<button
  disabled={isDirty && !isValid}
  type="submit"
  className="btn btn-primary"
>
  Create Post
</button>

So, here, we picked isDirty and isValid from formState to build a lock that disables the button when a form field isDirty and the form is in invalid state. This way, the button is enabled only when the user has entered something and they are valid values.

React Hook Form: TypeScript Support#

React Hook Form inherently supports TypeScript. It means when we’re using React Hook Form in a TypeScript based application, it automatically infers appropriate TypeScript types for form entities. For example, it infers the type for form values from defaultValues.

If you want the form data to conform explicitly to a type, you can pass it to useForm():

const {
  register,
  handleSubmit,
  reset,
  formState: { errors, isValid, isDirty }
} = useForm<TPost>();

Here, we want the form to handle form data with the TPost type. And we are explicit about it.

React Hook Form has types support for all necessary entities and functions. Please refer to this section of RHF docs for relevant information.

React Hook Form: Next.js Specific Notes#

Next.js app router pages, by default, are rendered serverside for performance benefits. However, this default behavior renders static pages to the client. So, a form action in default rendered pages is not accessible from the browser.

For this reason, forms in Next.js and in particular, React Hook Form which needs dynamic rendering to run field validations, need to be rendered client side. This has to be done explicitly with the "use client" directive declared at the top of the component.

Next.js - React Hook Form Example App#

For more insight, you can explore the example Next.js app for this guide in this branch of the repository over here. Please follow the instructions on the README.md file for setting up and geting a copy running.