Pain-Free Forms with React Hook Form

Cover image

Sooner or later you will need to deal with forms in React! Let's check how to deal with things like form data-validations! 👀

In this tutorial, we will cover how to do work with forms and do data-validation in React. Using a library called react-hook-form.

If you are not sure how to use forms with React, check out these 2 basic React forms patterns.

react-hook-form is a library to make dealing with data in forms easy. Data validation is especially easy when using react-hook-form.

Overview:

Let's get into it right away! 💪

With Basic validation

Let's start with how the basic validation may look like. 👇

import React from 'react';

import useForm from 'react-hook-form';

const LoginFormWithValidation = () => {
  const { 
    handleSubmit, // Submit handler wrapper
     register,  // Register form fields
     errors  // Errors object including error messages
     } = useForm();

  const onSubmit = values => {
    console.log(values); // email & password input's values in an object.
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="email">Email:</label>
      <input
        name="email"
        type="email"
        aria-describedby="emailError"
        ref={register({
          required: 'Email Message Required Message', // Error message when field is left empty.
          pattern: { // Validation pattern
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
            message: 'invalid email address' // Error message when validation fails.
          }
        })}
      />
      {errors.email ? (
        //
        <span id="emailError">{errors.email.message}</span>
      ) : (
        ''
      )}

      <label htmlFor="password">Password:</label>
      <input
        name="password"
        type="password"
        ref={register({
          validate: value => value !== 'test123' || 'Too common password, you can do better!' // Validation error message
        })}
        aria-describedby="passwordError"
      />
      {errors.password ? (
        <span id="passwordError">{errors.password.message}</span>
      ) : (
        ''
      )}

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

export default LoginFormWithValidation;

At this point, the main focus is the validation. For now, we will be using regular HTML inputs here. 😇

Let see next how we can share the state & validation rules across nested form elements.

Validation For Nested Fields

react-hook-form takes advantage of React Context. It allows you to provide a form context using FormContext. And read the context using a hook called useFormContext.

Since we are talking about the Context API! Check out how to do state management in React using React hooks and the Context API. 🤓

import React from 'react';

import useForm, { FormContext } from 'react-hook-form';
import EmailInput from './EmailInput';

const LoginFormWithNestedInput = () => {
  const methods = useForm();

  const onSubmit = values => {
    console.log(values);
  };

  return (
    // Initialise context with all form hook methods
    <FormContext {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        {/* EmailInput is a nested input field. */} 
        <EmailInput name="email"></EmailInput>
        <label htmlFor="password">Password:</label>
        <input
          name="password"
          type="password"
          ref={methods.register({
            validate: value => value !== 'test123' || 'You can do better'
          })}
          aria-describedby="passwordError"
        />
        {methods.errors.password ? (
          <span id="passwordError">{methods.errors.password.message}</span>
        ) : (
          ''
        )}

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

export default LoginFormWithNestedInput;

So far so good, what about the EmailInput component you wonder? 🧐

Here is how it looks like...

import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';

const EmailInput = props => {
  const { register, errors } = useFormContext(); // Regular form methods such as register is available from form context.
  return (
    <Fragment>
      <label htmlFor={props.name}>Email:</label>
      <input
        name={props.name}
        type="email"
        aria-describedby={`${props.name}-emailError`}
        ref={register({
          required: 'Required',
          pattern: {
            // Allows only nordschool.com emails
            value: /^[A-Z0-9._%+-]+@nordschool.com/i,
            message: 'Invalid email address - Only Nordschool domain is allowed'
          }
        })}
      />
      {errors[props.name] ? (
        <span id={`${props.name}-emailError`}>
          {errors[props.name].message}
        </span>
      ) : (
        ''
      )}
    </Fragment>
  );
};

export default EmailInput;

A common use-case for form validations is to use validation schemas.

Validation Schemas

react-hook-form allows declaring validation schemas. Using another validation library called yup we can define validation rules.

Yup is a JS object schema validator and object parser. The API is similar to Joi but smaller and more performant which makes it a good-fit for client-apps.

Using this setup, here is how a simple sign-up form may look like 🙌....

import React from 'react';

import useForm, { FormContext } from 'react-hook-form';
import EmailInput from './EmailInput';
import AddressInputs, { AddressSchema } from './AddressInputs';

import { string as yupstring, object as yupobject } from 'yup';

const SignupFormSchema = yupobject().shape({
  email: yupstring()
    .required('Email is unfortunately required')
    .email('Please add a real email'),
  name: yupstring().required('Name is important, what should we call you?'),
  ...AddressSchema // Custom schema imported from address inputs.
});

const SignupForm = () => {
  const methods = useForm({ validationSchema: SignupFormSchema });

  const onSubmit = values => {
    console.log(values);
  };

  return (
    <FormContext {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <label htmlFor="name"> Name:</label>
        <input
          name="name"
          type="text"
          aria-describedby="nameError"
          ref={methods.register()}
        />
        {methods.errors.name ? (
          <span id="nameError">{methods.errors.name.message}</span>
        ) : (
          ''
        )}
        <EmailInput name="email"></EmailInput>
        <AddressInputs name="email"></AddressInputs>
        <input type="submit" value="Search" />
      </form>
    </FormContext>
  );
};

export default SignupForm;

And the AddressInputs with their custom schema...

import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';

import { string as yupstring } from 'yup';

export const AddressSchema = {
  streetAddress: yupstring().required('Street address is required!'),
  postalCode: yupstring()
    .length(4)
    .required('required!'),
  city: yupstring().required('City is required!')
};

const AddressInputs = props => {
  const { register, errors } = useFormContext();
  return (
    <Fragment>
      <label htmlFor="stressAddress">Street Address:</label>
      <input
        name="streetAddress"
        type="text"
        aria-describedby="streetAddressError"
        ref={register()}
      />
      {errors.streetAddress ? (
        <span id="streetAddressError">{errors.streetAddress.message}</span>
      ) : (
        ''
      )}
      <label htmlFor="postalCode">Postal Code:</label>
      <input
        name="postalCode"
        type="text"
        aria-describedby="postalCodeError"
        ref={register()}
      />
      {errors.postalCode ? (
        <span id="postalCodeError">{errors.postalCode.message}</span>
      ) : (
        ''
      )}

      <label htmlFor="city">City:</label>
      <input
        name="city"
        type="text"
        aria-describedby="cityError"
        ref={register()}
      />
      {errors.city ? <span id="cityError">{errors.city.message}</span> : ''}
    </Fragment>
  );
};

export default AddressInputs;

If you noticed we are repeating that input field pattern all over the place! Let's encapsulate the input field elements in an own component. 😎

Custom Input Field

import React, { Fragment } from 'react';

const InputField = props => {
  return (
    <Fragment>
      <label htmlFor={props.name}>{props.label}</label>
      <input
        name={props.name}
        type={props.type || 'text'}
        aria-describedby={`${props.name}Error`}
        ref={props.registerFn}
      />
      {props.error ? (
        <span id={`${props.name}Error`}>{props.error.message}</span>
      ) : (
        ''
      )}
    </Fragment>
  );
};

export default InputField;

Now our AddressInputs could be refactored to look more like this...

import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';
import InputField from './InputField';
import { string as yupstring } from 'yup';

export const AddressSchema = {
  streetAddress: yupstring().required('Street address is required!'),
  postalCode: yupstring()
    .length(4)
    .required('required!'),
  city: yupstring().required('City is required!')
};

const AddressInputs = props => {
  const { register, errors } = useFormContext();
  return (
    <Fragment>
      <InputField
        label="Street Address:"
        name="stressAddress"
        error={errors.streetAddress}
        registerFn={register()}
      ></InputField>
      <InputField
        label="Postal Code:"
        name="postalCode"
        error={errors.postalCode}
        registerFn={register()}
      ></InputField>
      <InputField
        label="City:"
        name="stressAddress"
        error={errors.city}
        registerFn={register()}
      ></InputField>
    </Fragment>
  );
};

export default AddressInputs;

Much nicer! 👌

Ok so far so good, now you know enough to get you pretty far!

What about using a component library?

Let have a quick peek on how we can use react-hook-form and material-ui together.

Validation with Material-UI

import React from 'react';
import useForm from 'react-hook-form';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';

import { string as yupstring, object as yupobject } from 'yup';

const ContactFormSchema = yupobject().shape({
  email: yupstring()
    .required('Email is required')
    .email('Please enter a valid email'),
  message: yupstring().required('Please tell us how we can help you'),
  name: yupstring().required('Name is important, what should we call?')
});

const ContactForm = () => {
  const { register, errors, handleSubmit } = useForm({
    validationSchema: ContactFormSchema
  });

  const onSubmit = values => console.log(values);
  return (
    <form autoComplete="off" onSubmit={handleSubmit(onSubmit)} noValidate>
      <TextField
        id="name"
        label="Name"
        name="name"
        inputRef={register}
        placeholder="Joe"
        margin="normal"
        variant="outlined"
        error={errors.name ? true : false}
        helperText={errors.name ? errors.name.message : ''}
      />
      <TextField
        id="email"
        label="Email"
        name="email"
        inputRef={register}
        placeholder="example@nordschool.com"
        margin="normal"
        variant="outlined"
        error={errors.email ? true : false}
        helperText={errors.email ? errors.email.message : ''}
      />
      <TextField
        required
        id="message"
        multiline
        rows="4"
        name="message"
        inputRef={register}
        label="How can we help you today?"
        placeholder="Some pizza please!"
        margin="normal"
        variant="outlined"
        error={errors.message ? true : false}
        helperText={errors.message ? errors.message.message : ''}
      />
      <Button variant="contained" type="submit">
        Submit
      </Button>
    </form>
  );
};

export default ContactForm;

That is it, now your basic form-validation training is complete! 🎖️

Support us

Enjoyed the article? Share the summary thread on twitter.



Author image

Learn how to build scalable, fast and accessible web applications.

Follow us on Twitter

hello@nordschool.com