Next JS with React hooks forms || building forms

Building forms with Next JS

This is a quick example of how to setup form validation in Next.js with the React Hook Form library.

React Hook Form is a library for working with forms in React using React Hooks, I stumbled across it about a year ago and have been using it in my Next.js and React projects since then, I think it's easier to use than the other options available and requires less code. For more info see https://react-hook-form.com.

Installation Installing React Hook Form only takes a single command and you're ready to roll.

npm install react-hook-form

Example The following code excerpt demonstrates a basic usage example:

import React from 'react';
import { useForm } from 'react-hook-form';

export default function App() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm();
  const onSubmit = (data) => console.log(data);

  console.log(watch('example')); // watch input value by passing the name of it

  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register('example')} />

      {/* include validation with required or other standard HTML validation rules */}
      <input {...register('exampleRequired', { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}

The example is a simple registration form with pretty standard fields for title, first name, last name, date of birth, email, password, confirm password and an accept terms and conditions checkbox. All fields are required including the checkbox, the dob must be a valid date, the email address must be in a valid format, the password field must have a min length of 6, and the confirm password and password fields must match.

Here it is in action: (See on CodeSandbox at https://codesandbox.io/s/next-js-form-validation-example-forked-1e2soi)

Next.js Home Page with React Hook Form The home page contains an example registration form built with the React Hook Form library.

Form validation rules are defined with the Yup schema validation library and passed to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

Form validation rules are defined with the Yup schema validation library and passed with the formOptions to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

The onSubmit() method is called when the form is valid and submitted, and simply displays the form data in a javascript alert.

The returned JSX template contains the form with all of the input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('title')}).

To install React Hook Form, run the following command:

npm install react-hook-form

How to use React Hooks in a form In this section, you will learn about the fundamentals of the useForm Hook by creating a very basic registration form.

First, import the useForm Hook from the react-hook-form package:

import { useForm } from "react-hook-form";
// Then, inside your component, use the Hook as follows:

const { register, handleSubmit } = useForm();

The useForm Hook returns an object containing a few properties. For now, you only require register and handleSubmit.

The register method helps you register an input field into React Hook Form so that it is available for the validation, and its value can be tracked for changes.

To register the input, we’ll pass the register method into the input field as such:

<input type="text" name="firstName" {...register('firstName')} />

This spread operator syntax is a new implementation to the library that enables strict type checking in forms with TypeScript. You can learn more about strict type checking in React Hook Form here.

Versions older than v7 had the register method attached to the ref attribute as such:

<input type="text" name="firstName" ref={register} />

Note that the input component must have a name prop, and its value should be unique.

The handleSubmit method, as the name suggests, manages form submission. It needs to be passed as the value to the onSubmit prop of the form component.

The handleSubmit method can handle two functions as arguments. The first function passed as an argument will be invoked along with the registered field values when the form validation is successful. The second function is called with errors when the validation fails.

const onFormSubmit  = data => console.log(data);
const onErrors = errors => console.error(errors);
<form onSubmit={handleSubmit(onFormSubmit, onErrors)}>
 {/* ... */}
</form>

Now that you have a fair idea about the basic usage of the useForm Hook, let’s take a look at a more realistic example:

import React from "react";
import { useForm } from "react-hook-form";

const RegisterForm = () => {
  const { register, handleSubmit } = useForm();
  const handleRegistration = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(handleRegistration)}>
      <div>
        <label>Name</label>
        <input name="name" {...register('name')} />
      </div>
      <div>
        <label>Email</label>
        <input type="email" name="email" {...register('email')} />
      </div>
      <div>
        <label>Password</label>
        <input type="password" name="password" {...register('password')} />
      </div>
      <button>Submit</button>
    </form>
  );
};
export default RegisterForm;

As you can see, no other components were imported to track the input values. The useForm Hook makes the component code cleaner and easier to maintain, and because the form is uncontrolled, you do not have to pass props like onChange and value to each input.

You can use any other UI library of your choice for creating the form. But first make sure to check the docs, and find the prop used for accessing reference attribute of the native input component.

In the next section, you will learn how to handle form validation in the form we just built.

How to validate forms with React Hook Form

To apply validations to a field, you can pass validation parameters to the register method. Validation parameters are similar to the existing HTML form validation standard.

These validation parameters include the following properties:

required indicates if the field is required or not. If this property is set to true, then the field cannot be empty minlength and maxlength set the minimum and maximum length for a string input value min and max set the minimum and maximum values for a numerical value type indicates the type of the input field; it can be email, number, text, or any other standard HTML input types pattern defines a pattern for the input value using a regular expression If you want to mark a field as required, you code should turn out like this:

<input name="name" type="text" {...register('name', { required: true } )} />

Now try submitting the form with this field empty. This will result in the following error object:

{
name: {
  type: "required",
  message: "",
  ref: <input name="name" type="text" />
  }
}

Here, the type property refers to the type of validation that failed, and the ref property contains the native DOM input element.

You can also include a custom error message for the field by passing a string instead of a boolean to the validation property:

<form onSubmit={handleSubmit(handleRegistration, handleError)}>
  <div>
      <label>Name</label>
      <input name="name" {...register('name', { required: "Name is required" } )} />
  </div>
</form>

Then, access the errors object by using the useForm Hook:

const { register, handleSubmit, formState: { errors } } = useForm();
You can display errors to your users like so:
//  {errors?.name && errors.name.message}

const RegisterForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const handleRegistration = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(handleRegistration)}>
      <div>
        <label>Name</label>
        <input type="text" name="name" {...register('name')} />
        {errors?.name && errors.name.message}
      </div>
      {/* more input fields... */}
      <button>Submit</button>
    </form>
  );
};

Below you can find the complete example:

import React from "react";
import { useForm } from "react-hook-form";

const RegisterForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const handleRegistration = (data) => console.log(data);
  const handleError = (errors) => {};

  const registerOptions = {
    name: { required: "Name is required" },
    email: { required: "Email is required" },
    password: {
      required: "Password is required",
      minLength: {
        value: 8,
        message: "Password must have at least 8 characters"
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(handleRegistration, handleError)}>
      <div>
        <label>Name</label>
        <input name="name" type="text" {...register('name', registerOptions.name) }/>
        <small className="text-danger">
          {errors?.name && errors.name.message}
        </small>
      </div>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          {...register('email', registerOptions.email)}
        />
        <small className="text-danger">
          {errors?.email && errors.email.message}
        </small>
      </div>
      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          {...register('password', registerOptions.password)}
        />
        <small className="text-danger">
          {errors?.password && errors.password.message}
        </small>
      </div>
      <button>Submit</button>
    </form>
  );
};
export default RegisterForm;

If you want to validate the field when there is an onChange or onBlur event, you can pass a mode property to the useForm Hook:

const { register, handleSubmit, errors } = useForm({
  mode: "onBlur"
});

React Hook Form provides the wrapper Controller component that allows you to register a controlled external component, similar to how the register method works. In this case, instead of the register method, you will use the control object from the useForm Hook:

const { register, handleSubmit, control } = useForm();

Consider that you have to create a role field in your form that will accept values from a select input. You can create the select input using the react-select library.

The control object should be passed to the control prop of the Controller component, along with the name of the field. You can specify the validation rules using the rules prop.

The controlled component should be passed to the Controller component using the as prop. The Select component also requires an options prop to render the drop-down options:

<Controller
  name="role"
  control={control}
  defaultValue=""
  rules={registerOptions.role}
  render={({ field }) => (
    <Select options={selectOptions} {...field} label="Text field" />
  )}
/>

The render prop above provides onChange, onBlur, name, ref, and value to the child component. By spreading field into the Select component, React Hook Form registers the input field.

You can check out the complete example for the role field below:

import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
// ...
const { register, handleSubmit, errors, control } = useForm({
  // use mode to specify the event that triggers each input field 
  mode: "onBlur"
});

const selectOptions = [
  { value: "student", label: "Student" },
  { value: "developer", label: "Developer" },
  { value: "manager", label: "Manager" }
];

const registerOptions = {
  role: { required: "Role is required" }
};
<form>
  <div>
    <label>Your Role</label>
    <Controller
      name="role"
      control={control}
      defaultValue=""
      rules={registerOptions.role}
      render={({ field }) => (
        <Select options={selectOptions} {...field} label="Text field" />
      )}
    />
    <small className="text-danger">
      {errors?.role && errors.role.message}
    </small>
  </div>
</form>

You can also go through the API reference for the Controller component here for a detailed explanation.

Lets check some more examples with yup for validation

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

export default Home;

function Home() {
  // form validation rules
  const validationSchema = Yup.object().shape({
    title: Yup.string().required('Title is required'),
    firstName: Yup.string().required('First Name is required'),
    lastName: Yup.string().required('Last name is required'),
    dob: Yup.string()
      .required('Date of Birth is required')
      .matches(
        /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/,
        'Date of Birth must be a valid date in the format YYYY-MM-DD'
      ),
    email: Yup.string()
      .required('Email is required')
      .email('Email is invalid'),
    password: Yup.string()
      .min(6, 'Password must be at least 6 characters')
      .required('Password is required'),
    confirmPassword: Yup.string()
      .oneOf([Yup.ref('password'), null], 'Passwords must match')
      .required('Confirm Password is required'),
    acceptTerms: Yup.bool().oneOf([true], 'Accept Ts & Cs is required'),
  });
  const formOptions = { resolver: yupResolver(validationSchema) };

  // get functions to build form with useForm() hook
  const { register, handleSubmit, reset, formState } = useForm(formOptions);
  const { errors } = formState;

  function onSubmit(data) {
    // display form data on success
    alert('SUCCESS!! :-)\n\n' + JSON.stringify(data, null, 4));
    return false;
  }

  return (
    <div className="card m-3">
      <h5 className="card-header">Next.js - Form Validation Example</h5>
      <div className="card-body">
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="form-row">
            <div className="form-group col">
              <label>Title</label>
              <select
                name="title"
                {...register('title')}
                className={`form-control ${errors.title ? 'is-invalid' : ''}`}
              >
                <option value=""></option>
                <option value="Mr">Mr</option>
                <option value="Mrs">Mrs</option>
                <option value="Miss">Miss</option>
                <option value="Ms">Ms</option>
              </select>
              <div className="invalid-feedback">{errors.title?.message}</div>
            </div>
            <div className="form-group col-5">
              <label>First Name</label>
              <input
                name="firstName"
                type="text"
                {...register('firstName')}
                className={`form-control ${errors.firstName ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.firstName?.message}</div>
            </div>
            <div className="form-group col-5">
              <label>Last Name</label>
              <input
                name="lastName"
                type="text"
                {...register('lastName')}
                className={`form-control ${errors.lastName ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.lastName?.message}</div>
            </div>
          </div>
          <div className="form-row">
            <div className="form-group col">
              <label>Date of Birth</label>
              <input
                name="dob"
                type="date"
                {...register('dob')}
                className={`form-control ${errors.dob ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.dob?.message}</div>
            </div>
            <div className="form-group col">
              <label>Email</label>
              <input
                name="email"
                type="text"
                {...register('email')}
                className={`form-control ${errors.email ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.email?.message}</div>
            </div>
          </div>
          <div className="form-row">
            <div className="form-group col">
              <label>Password</label>
              <input
                name="password"
                type="password"
                {...register('password')}
                className={`form-control ${errors.password ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.password?.message}</div>
            </div>
            <div className="form-group col">
              <label>Confirm Password</label>
              <input
                name="confirmPassword"
                type="password"
                {...register('confirmPassword')}
                className={`form-control ${errors.confirmPassword ? 'is-invalid' : ''}`}
              />
              <div className="invalid-feedback">{errors.confirmPassword?.message}</div>
            </div>
          </div>
          <div className="form-group form-check">
            <input
              name="acceptTerms"
              type="checkbox"
              {...register('acceptTerms')}
              id="acceptTerms"
              className={`form-check-input ${errors.acceptTerms ? 'is-invalid' : ''}`}
            />
            <label htmlFor="acceptTerms" className="form-check-label">
              Accept Terms & Conditions
            </label>
            <div className="invalid-feedback">{errors.acceptTerms?.message}</div>
          </div>
          <div className="form-group">
            <button type="submit" className="btn btn-primary mr-1">
              Register
            </button>
            <button type="button" onClick={() => reset()} className="btn btn-secondary">
              Reset
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

Another Example

export function InputForm({ services, handleFormSubmit, servicesLocations, serviceType, userProfile }: inputProps) {

  const validationSchema = Yup.object().shape({
    orderBy: Yup.string().required('order by is required'),
    phoneNumber: Yup.string().required('phone is required'),
    orderDate: Yup.string().required('order date is required'),
    regarding: Yup.string().required('regarding is required'),
  });
  const formOptions: any = { resolver: yupResolver(validationSchema), reValidateMode: 'onChange', mode: 'onChange' };

  const { register, getValues, handleSubmit, formState } = useForm<any>(formOptions);
  const { errors } = formState;

  const onSubmit = (e: any) => {
    const { orderDate, orderBy, instructions, extension, regarding, phoneNumber } = getValues();
    // submit
  };

 return (
   <form onSubmit={handleSubmit(onSubmit)} className="px-4 py-5 sm:p-6">
              <div className="grid grid-cols-6 gap-6 pb-8 border-b-2 border-pc-base-black">
                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-800 font-source">Ordered By</label>
                  <input
                    defaultValue={userProfile.name}
                    className={`w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300 ${errors.orderBy ? 'is-invalid' : ''
                      }`}
                    {...register('orderBy')}
                    type="text"
                    name="orderBy"
                    id="orderBy"
                    readOnly
                    autoComplete="orderBy"
                  />
                  {errors.orderBy?.message && <FormError errorMessage={errors.orderBy?.message} />}
                </div>

                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-700 font-source">Ordered date</label>
                  <input
                    {...register('orderDate')}
                    defaultValue={getToday()}
                    type="text"
                    name="orderDate"
                    readOnly
                    id="orderDate"
                    autoComplete="orderDate"
                    className={`w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300 ${errors.orderDate ? 'is-invalid' : ''
                      }`}
                  />
                  {errors.orderDate?.message && <FormError errorMessage={errors.orderDate?.message} />}
                </div>

                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-700 font-source">Phone Number</label>
                  <input
                    defaultValue={userProfile.phoneNumber}
                    {...register('phoneNumber')}
                    type="text"
                    name="phoneNumber"
                    readOnly
                    id="phoneNumber"
                    autoComplete="phoneNumber"
                    className={`w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300 ${errors.phoneNumber ? 'is-invalid' : ''
                      }`}
                  />
                  {errors.phoneNumber?.message && <FormError errorMessage={errors.phoneNumber?.message} />}
                </div>

                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-700 font-source">Extension</label>
                  <input
                    type="text"
                    name="extension"
                    id="extension"
                    autoComplete="extension"
                    className="w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300"
                  />
                </div>

                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-700 font-source">Medical Record Number</label>
                  <input
                    {...register('regarding')}
                    type="text"
                    name="regarding"
                    id="regarding"
                    placeholder='MRN #'
                    autoComplete="regarding"
                    className={`w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300 ${errors.regarding ? 'is-invalid' : ''
                      }`}
                  />
                  {errors.regarding?.message && <FormError errorMessage={errors.regarding?.message} />}
                </div>
                <div className="col-span-6 sm:col-span-3">
                  <label className="block text-sm font-bold text-gray-700 font-source">Special Instructions</label>
                  <input
                    type="text"
                    name="instructions"
                    id="instructions"
                    autoComplete="instructions"
                    className="w-full p-2 mt-2 border-2 rounded shadow-sm outline-none border-pc-base-black focus:border-gray-300"
                  />
                </div>
              </div>
            </div>
          </form>
 )

Conclusion

I am using react hook forms for all my projects and its works like charm, it provided whatever we need, we can also look at formik library in react for form validation and form-handling.

References

Comments