How to Create Custom Form Validation in React with Yup

How to create custom form validation in React with Yup feature image

When building forms you have to make sure all fields are filled correctly. There are multiple solutions for this, aside to the basic HTML form validation. One of these options is a library called Yup. This tutorial will show you how to use Yup to create custom form validation for forms build with React.

You can find demo for this tutorial on my Codesandbox.

A word on form validation

There are multiple ways to solve the problem with form validation. The most basic and also most accessible is the native way. This is the validation provided by browsers. This validation works well if you use correct field types and don’t need any customization. Then, there are bigger, all-in-one solutions, such as Formik.

These solutions offer a lot of flexibility and customization. They are often also more developer-friendly. The downside is that they are also heavier, or bigger, and often require deep implementation. If all you need is just one thing, such as validation, it may not be a reason to rewrite your solution to some framework.

The fact is, you don’t have to do that. There is also the third option. There are libraries focused on helping with just one thing, such as form validation, and won’t interfere with other things. One of these libraries is Yup. This library helps with validation of any kind, including forms.

Validation with Yup

The way Yups works is simple. You start by defining a schema. This is an object that specifies all values you want to check. It also specifies characteristics of each of these values. For example, you can define that you want to check a value for an email address. In schema, you can call this value email. This will be the key on the schema object.

Next, you can specify that this email value must be a string. Besides that, you can also specify that it should be “type” of an email. This means that Yup will, as part of validation, test that string if it is in an actual email format. Since you may really need that email, for whatever reason, you can also specify that it is required.

There are many other options. You can also specify that something is a URL, or that the value can contain only numbers, or that it has to contain at least eight characters. Whatever custom validation rule you need chances are Yup will be able to help you.

A quick introduction

This tutorial will show you two things. First, it will show you how to create a simple form in React. Second, it will show you how to use Yup library to put together custom validation for custom React form. The form we will build will use useState hook for state management. We will also use memo and useCallback hooks.

A note on dependencies

This tutorial uses the create-react-app as the starting template. The react and react-dom dependencies are both version 17.0.2. The react-scripts is version 4.0.0. The yup library is the fourth dependency and it is version 0.32.9. The fifth and last dependency is immutability-helper, version 3.1.1.

This dependency helps to mutate a copy of data without changing the original source. You will use this dependency to update form states for values and errors. That’s it. Now, let’s get to the tutorial.

The form field component

The first component we need to build is a form field. This will be a simple component. It will render fieldset that will contain label and input, and simple error message. Each field will receive some data through props: onFieldChange, labelText, fieldType, fieldName, fieldValue and hasError.

The onFieldChange is handler for input change event. To avoid using arrow function in render, we will create new handler for change event in the field component. This handler will call the onFieldChange function passed through props with the fieldName of the current field component and onChange event from the input passed as arguments.

Next, it will use the labelText to render custom input label and the hasError to show error message when appropriate. The fieldType will specify the type of input we want to render. The fieldName will specify the name and id attributes and help us pair the input with values and errors state. The fieldValue will pass the input value.

// Import memo and useCallback hooks:
import { memo, useCallback } from 'react'

// Create the Field component:
export const Field = memo((props) => {
  // Create handler for change event:
  const onFieldChange = useCallback(
    (event) => {
      props.onFieldChange(props.fieldName, event.target.value)
    },
    [props.onFieldChange, props.fieldName]
  )

  // Render all HTML components:
  return (
    <fieldset>
      <label htmlFor={props.fieldName}>{props.labelText}</label>

      <input
        type={props.fieldType}
        name={props.fieldName}
        id={props.fieldName}
        onChange={onFieldChange}
        value={props.fieldValue}
      />

      {props.hasError && (
        <p>{`Please fill in correct value for "${props.labelText}".`}</p>
      )}
    </fieldset>
  )
})

The form component

The form component will be just a wrapper that renders individual <Field /> components. It will accept values and errors states (objects) and onSubmit handler through props. Properties of values and errors states will be appropriately spread between individual <Field /> components.

// Import memo hook:
import { memo } from 'react'

// Import Field component:
import { Field } from './form-field'

// Create the Field component:
export const Form = memo((props) => (
  <form onSubmit={props.onSubmit} noValidate>
    <Field
      labelText="First name"
      fieldType="text"
      fieldName="firstName"
      fieldValue={props.values.firstName}
      hasError={props.errors.firstName}
      onFieldChange={props.onFieldChange}
    />

    <Field
      labelText="Last name"
      fieldType="text"
      fieldName="lastName"
      fieldValue={props.values.lastName}
      hasError={props.errors.lastName}
      onFieldChange={props.onFieldChange}
    />

    <Field
      labelText="Email"
      fieldType="email"
      fieldName="email"
      fieldValue={props.values.email}
      hasError={props.errors.email}
      onFieldChange={props.onFieldChange}
    />

    <Field
      labelText="Password (+8 characters)"
      fieldType="password"
      fieldName="password"
      fieldValue={props.values.password}
      hasError={props.errors.password}
      onFieldChange={props.onFieldChange}
    />

    <Field
      labelText="Personal website"
      fieldType="url"
      fieldName="website"
      fieldValue={props.values.website}
      hasError={props.errors.website}
      onFieldChange={props.onFieldChange}
    />

    <button type="submit">Send</button>
  </form>
))

The App component

The App component will be the most complex. It will contain all the logic for the form.

The schema

First, we will create new schema object with Yup. This schema will define all values (form fields) we want to validate. These values will be firstName, lastName, email, password and website. We will want all these values to be string() and required(). We will specify the email value to match email format, with email().

We will also specify that password has to be at least 8 characters long with min(8). Lastly, we will specify that the website has match URL format, with url().

// Create validation schema:
const formSchema = yup.object().shape({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
  website: yup.string().url().required(),
})

States

The App component will contain two states, one for form values and one for form errors. Both states will be objects with keys that match keys in formSchema and fieldName property on <Field /> components. Initial values for form values will be empty strings. Initial values for form errors will be false.

// ... previous code

export const App = memo(() => {
  // Create state for form values:
  const [values, setValues] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    website: '',
  })
  // Create state for form errors:
  const [errors, setErrors] = useState({
    firstName: false,
    lastName: false,
    email: false,
    password: false,
    website: false,
  })

  // ... rest of the code
}

Input field change event handler

The App component will also define the change handler function that will be passed through the <Form /> component to individual <Field /> components. This handler will use setter method for values state and update method from immutability-helper to update value of a specific state key (field name).

The function will accept both, key (field name) and value to save in the state, as parameters. The immutability-helper will ensure we are not updating any value directly and working with copies, not originals.

export const App = memo(() => {
  // ... previous code

  // Create handler for input change event:
  const onFieldChange = useCallback((fieldName, value) => {
    setValues((prevValues) =>
      update(prevValues, {
        [fieldName]: {
          $set: value,
        },
      })
    )
  }, [])

  // ... rest of the code

Form submit event handler

The Yup library works with promises. This means that we can either use then() handler methods or async/await syntax to work with validation results. For now, we will use the async/await syntax to avoid unnecessary nesting with then() methods. First, we will declare the onSubmit function as async.

This will allow us to use the await keyword inside this function when we will work with promises. The first thing the onSubmit function will do is to prevent form submission with event.preventDefault(). Next, we will check if form is valid by calling isValid() method on schema for our form, assigned to formSchema variable.

We will pass two arguments to the isValid() method. The first will be the values state, object with all form fields and corresponding values. Second will be options object where we will set the abortEarly option to false. This means that if Yup encounters any error during validation it will not stop the process.

It will stop only after all form values are validated. Only then will it return the status for all specified values (form fields). Without this option, Yup would stop after first error and return only that. So, if there were multiple fields with errors, we would not know about it. We would know only about the first error Yup found.

We will assign the call to isValid() method to a variable. This method returns a promise. So, we will use the await keyword to pause the execution and wait for the promise to resolve and return some value. Next, we will check if the resolved value is true. If it is, it means the form is valid. You can do whatever you need to submit the values.

If the resolved value is false it can mean one of two things. It can mean that some value is missing, that some field is empty. Or it can mean that some value is in a wrong format. For example, password contains less than 8 characters or URL is not in a correct format. Thing is, we need to know what field has some error.

In order to get these errors we will call validate() method on the schema object assigned to formSchema variable. We will pass the same two arguments as to the isValid() method: values state object and abortEarly set to false. This method also returns a promise. However, this promise doesn’t resolve with errors, but rejects.

This means we will need the catch() handler function to get those errors. The error object returned by the promise contains property inner. The value of this property is an array with all errors and details about them. We don’t need all those details. We need just the name of the field so we know for which field we should show an error.

To reduce the amount of information for every error we will use the reduce() method. We will reduce each error object to a simple object where field name will be the key and true will be its value. After this, we will use the immutability-helper to update the errors state.

export const App = memo(() => {
  // ... previous code

  // Create handler for form submit event:
  const onSubmit = useCallback(
    async (event) => {
      // Prevent form from submitting:
      event.preventDefault()

      // Check the schema if form is valid:
      const isFormValid = await formSchema.isValid(values, {
        abortEarly: false, // Prevent aborting validation after first error
      })

      if (isFormValid) {
        // If form is valid, continue submission.
        console.log('Form is legit')
      } else {
        // If form is not valid, check which fields are incorrect:
        formSchema.validate(values, { abortEarly: false }).catch((err) => {
          // Collect all errors in { fieldName: boolean } format:
          const errors = err.inner.reduce((acc, error) => {
            return {
              ...acc,
              [error.path]: true,
            }
          }, {})

          // Update form errors state:
          setErrors((prevErrors) =>
            update(prevErrors, {
              $set: errors,
            })
          )
        })
      }
    },
    [values]
  )

  // ... rest of the code

Putting the App component together

Now, we can put all these pieces for the logic together, add the render part with <Form /> component, and we are done, almost.

// Import memo, useCallback and useState hooks:
import { memo, useCallback, useState } from 'react'

// Import update method and yup:
import update from 'immutability-helper'
import * as yup from 'yup'

// Import Form component:
import { Form } from './form'

// Create validation schema:
const formSchema = yup.object().shape({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
  website: yup.string().url().required(),
})

// Create the App component:
export const App = memo(() => {
  // Create state for form values:
  const [values, setValues] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    website: '',
  })
  // Create state for form errors:
  const [errors, setErrors] = useState({
    firstName: false,
    lastName: false,
    email: false,
    password: false,
    website: false,
  })

  // Create handler for input change event:
  const onFieldChange = useCallback((fieldName, value) => {
    setValues((prevValues) =>
      update(prevValues, {
        [fieldName]: {
          $set: value,
        },
      })
    )
  }, [])

  // Create handler for form submit event:
  const onSubmit = useCallback(
    async (event) => {
      // Prevent form from submitting:
      event.preventDefault()

      // Check the schema if form is valid:
      const isFormValid = await formSchema.isValid(values, {
        abortEarly: false, // Prevent aborting validation after first error
      })

      if (isFormValid) {
        // If form is valid, continue submission.
        console.log('Form is legit')
      } else {
        // If form is not valid, check which fields are incorrect:
        formSchema.validate(values, { abortEarly: false }).catch((err) => {
          // Collect all errors in { fieldName: boolean } format:
          const errors = err.inner.reduce((acc, error) => {
            return {
              ...acc,
              [error.path]: true,
            }
          }, {})

          // Update form errors state:
          setErrors((prevErrors) =>
            update(prevErrors, {
              $set: errors,
            })
          )
        })
      }
    },
    [values]
  )

  // Render the form:
  return (
    <Form
      values={values}
      errors={errors}
      onFieldChange={onFieldChange}
      onSubmit={onSubmit}
    />
  )
})

The main component (index)

There is one last thing to do. We need to create the component that will render everything we’ve built so far. This main component will import the <App /> component and render it in a rootElement, which will be div in the main HTML file. Now we are done.

// Import React StrictMode and ReactDOM:
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'

// Import App component:
import { App } from './app'

// Import any CSS styles:
import './styles.css'

// Render the React app in the DOM:
const rootElement = document.getElementById('root')
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
)

Conclusion: How to create custom form validation in React with Yup

Creating custom form validation doesn’t have to be hard. Nor does it require using frameworks and all-in-one solutions. There are also smaller libraries, such as Yup, that will help you create custom validation for your forms easily and quickly. I hope that this tutorial helped you understand how to do this.

If you liked this article, please subscribe so you don't miss any future post.

If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂

By Alex Devero

I'm Founder/CEO of DEVERO Corporation. Entrepreneur, designer, developer. My mission and MTP is to accelerate the development of humankind through technology.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.