How to Create Custom Form Validation in React with Yup
Tags: webdev, tutorial, react, javascript
Published: 2021-09-13 07:08:37

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.

```jsx
// 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.

```jsx
// 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()`.

```jsx
// 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`.

```jsx
// ... 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.

```jsx
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.

```jsx
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.

```jsx
// 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.

```jsx
// 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.

[codesandbox]: https://codesandbox.io/s/react-form-with-yup-validation-l1scz
[field types]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#input_types
[formik]: https://formik.org
[yup]: https://github.com/jquense/yup
[usestate hook]: https://blog.alexdevero.com/react-usestate-hook-in-action/
[memo]: https://reactjs.org/docs/react-api.html#reactmemo
[usecallback]: https://blog.alexdevero.com/react-usecallback-hook/
[create-react-app]: https://create-react-app.dev
[immutability-helper]: https://github.com/kolodny/immutability-helper
[arrow function in render]: https://reactjs.org/docs/faq-functions.html#arrow-function-in-render
[then()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
[catch()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[async/await]: https://blog.alexdevero.com/javascript-async-await/
[reduce()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce