DEV Community

loading...
Cover image for Pain-Free Forms with React Hook Form
Nordschool

Pain-Free Forms with React Hook Form

ahmaman profile image Ahmed Mansour Originally published at nordschool.com ・6 min read

Originally posted on Nordschool.

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

Enjoyed the article? Share the summary thread on twitter.


Better Code Monday Newsletter

You might also like my newsletter. The idea is to share 3 web dev tips every Monday.

My goal is to improve my writing skills and share knowledge as much as possible. So far, few hundreds of developers have subscribed and seem to like it.

To get a feeling of what kind of stuff I share, Check out the previous newsletter issues and subscribe.

Discussion (1)

pic
Editor guide
Collapse
dastasoft profile image
dastasoft

Thank you for sharing this, the useFormContext it's very very handy!