Form State Management & Validation in React (Part 3): Using Yup with React Hook Form

Form State Management & Validation in React (Part 3): Using Yup with React Hook Form

ยท

9 min read

In the previous article, we discussed how to use the React Hook Form library to manage form state and validate the data users input in our forms before submission. The latter is meant to enforce data integrity by ensuring only data of a valid kind is submitted to the API.

However, while React Hook Form offers a flexible approach to form validation in React applications, it may have some limitations. Two standout ones are:

  1. Limited Validation Rules: React Hook Form provides basic validation rules out of the box, such as required fields, pattern matching, and minimum/maximum length of inputs. However, it lacks an extensive set of validation rules and schema-based validation with support for various data types, conditional validation, and nested object validation, providing more comprehensive validation capabilities.

  2. Schema-based Validation: Since validation logic is typically defined within an individual input field's register method in React Hook Form, it does not excel in schema-based validation. Schema-based validation is where as a developer, you define the data type you expect to be passed as the values for different data structures. The schema can be made reusable, eliminating the need for repetitive code. For example, you could define a single validation rule for passwords in your entire application and then reuse that rule whenever you want a user to input their password in a form (e.g. when logging in, signing up or resetting their password). This way, without repeating the same code over and over again, we can maintain a single schema definition for our model and ensure consistency and reusability across form components.

    React Hook Form is very limited when it comes to schema-based validation making it hard to share validation rules between different parts of your application.

While React Hook Form offers an efficient solution for form validation in React applications, integrating Yup with React Hook Form can enhance these capabilities by providing a richer set of validation features, better support for complex data structures, and improved handling of asynchronous validation logic.

Getting started with Yup

We need to install the Yup validation library and the Hookform Resolvers library in our project. We will define the validation schema in the former and use the latter to tell React Hook Form how to enforce the validation in our form.

npm install yup @hookform/resolvers

We are using the form we finished the previous article with:

The first thing we do is import Yup into our form component and the yupResolver from Hookform Resolvers library. This informs React Hook Form to apply Yup's validation structure.

import * as Yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

Next, let us define validation rules for the password field.

Writing a Yup validation for input fields.

Writing validation using Yup involves defining a schema, which is just an object, that describes the structure and validation rules for your form data. You can specify various validation rules for each field, such as required fields, data types accepted, string lengths etc. For example, Yup.string().required() specifies that the field must be a string and cannot be empty.

Additionally, you can use chaining methods like min() or max() to specify minimum and maximum lengths for strings, or matches() to enforce a specific pattern using regular expressions.

Currently, we have this as our validation for the password input:

<input
    {...register('password', {
        required: {
            value: true,
            message: 'Your password is required',
        },
        pattern: {
            value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%]).{8,24}$/,
            message: (
                <>
                    8 to 24 characters <br />
                    Must include upper and lowercase letters, a number, and a
                    special character. <br />
                    Allowed special characters:{' '}
                    <span aria-label="exclamation mark">!</span>
                    <span aria-label="at symbol">@</span>
                    <span aria-label="hashtag">#</span>
                    <span aria-label="dollar sign">$</span>
                    <span aria-label="percent">%</span>
                </>
            ),
        },
        minLength: {
            value: 8,
            message: 'Password must be at least 8 characters',
        },
        maxLength: {
            value: 24,
            message: 'Password must not exceed 24 characters',
        },
    })}
    type="text"
    placeholder="Password"
    id="password"
/>

We can replace all that React Hook Form validation code with this simple Yup validation:

const passwordSchema = Yup.string()
    .required('Your password is required')
    .matches(/[0-9]/, "Must include a number.")
    .matches(/[a-z]/, "Must include a lowercase letter.")
    .matches(/[A-Z]/, "Must include an uppercase letter.")
    .matches(/[!@#$%&*]/, "Must include special characters: ! @ # $ % or &")
    .min(8, 'Password must have at least 8 characters');

To break down the password validation syntax:

  1. Yup.string(): This specifies that the password should be a string.

  2. .required('Your password is required'): This sets the requirement that the password field must not be empty. If the field is empty, it will throw the custom error message in the parentheses: "Your password is required".

  3. .matches(/[0-9]/, "Must include a number."): This checks if the password contains at least one numeric digit (0-9). If not, it will throw an error message "Must include a number."

  4. .matches(/[a-z]/, "Must include a lowercase letter."): This checks if the password contains at least one lowercase alphabetical character (a-z). If not, it will throw an error message "Must include a lowercase letter."

  5. .matches(/[A-Z]/, "Must include an uppercase letter."): This checks if the password contains at least one uppercase alphabetical character (A-Z). If not, it will throw an error message "Must include an uppercase letter."

  6. .matches(/[!@#$%&*]/, "Must include special characters: ! @ # $ % or &"): This checks if the password contains at least one special character from the provided set: ! @ # $ % or &. If not, it will throw an error message "Must include special characters: ! @ # $ % or &".

  7. .min(8, 'Password must have at least 8 characters'): This checks if the password length is at least 8 characters long. If it's shorter than 8 characters, it will throw an error message "Password must have at least 8 characters".

Next, we define the email field's validation:

const emailSchema = Yup.string()
    .required('Email is required')
    .email('Please enter a valid email address')
    .matches(
        /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        "Indicate your email provider's domain e.g .com or .co.ke"
    );

Linking the input validations with the form.

We finished defining the validation for individual form fields but have yet to link them with the actual fields inside the form. If you have worked with forms before, you already know that they are objects.

Just as we defined our input fields as strings, we do the same with our form, declaring it as an object and then defining the structure of that form's object.

The key values for each object property must correspond with the name each input field was initiated with by React Hook Form's register method. This links each field with the input validations above.

A schema object for the form is defined with this syntax:

const loginValidationSchema = Yup.object().shape({
    password: passwordSchema,
    email: emailSchema,
});

Integrate the validation with the Hookform Resolvers library.

The final step is to link our validation rules with the Hookform Resolvers library which as you remember, instructs React Hook Form how to enforce the validation in our form. We do this by passing a resolver function to the useForm hook to integrate the validation logic into our form.

A resolver is a method in React Hook Form responsible for validating form inputs using an external validation library such as Yup or Zod.

The resolver function takes the form data as input and returns an object containing the validation result, including any validation errors that may occur.

This is the step that allows React Hook Form to delegate the validation process to the Yup validation library as it focuses on handling form state.

const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm({
        resolver: yupResolver(loginValidationSchema),
    });

At this point, we can refactor the input field and remove all validation in the input fields. What we should be left with in the returned JSX is this concise code:

/* imports*/

const ValidatingReactHookFormWithYup = () => {
/*JS logic*/
    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <h1>Validating React Hook Form with Yup</h1>

            <label htmlFor="email">Email</label>
            <input {...register('email')} placeholder="Email" id="email" />
            {errors.email && <p className="show-message">{errors.email.message}</p>}

            <label htmlFor="password">Password</label>
            <input {...register('password')} placeholder="Password" id="password" />
            {errors.password && (
                <p className="show-message">{errors.password.message}</p>
            )}

            <button>Log In</button>
        </form>
    );
};

export default ValidatingReactHookFormWithYup;

Let us test if the validation works as expected:

Yes, it works just as expected.

Yup also allowed us to split the regex expression we previously had, with each pattern now being evaluated independently and returning its own custom error message in case of failure instead of throwing an entire paragraph full of errors. A user is now informed of their error one at a time.

We have successfully implemented Yup validation, but as a final step, we can abstract the entire validation schema into its own file, which allows us to reuse the rules in several components. As it is, we would have to rewrite the same validation code in a signup form component if we had it when it would be much smarter to reuse the code we already have.

This is the advantage of schema-based validation.

Let us create a validation.js file and move our schema there.

import * as Yup from 'yup';

const emailSchema = Yup.string()
    .required('Email is required')
    .email('Please enter a valid email address')
    .matches(
        /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        "Indicate your email provider's domain e.g .com or .co.ke"
    );

const passwordSchema = Yup.string()
    .required('Your password is required')
    .matches(/[0-9]/, 'Must include a number.')
    .matches(/[a-z]/, 'Must include a lowercase letter.')
    .matches(/[A-Z]/, 'Must include an uppercase letter.')
    .matches(/[!@#$%&*]/, 'Must include special characters: ! @ # $ % or &')
    .min(8, 'Password must have at least 8 characters')
    .max(24, 'Password must not exceed 24 characters');

export const loginValidationSchema = Yup.object().shape({
    password: passwordSchema,
    email: emailSchema,
});

By doing this, and exporting the loginValidationSchema, we simply import it in any component that needs it instead of needless rewriting. In our login form, we have it as:


import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { loginValidationSchema } from '../validation';

const ValidatingReactHookFormWithYup = () => {
    const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm({
        resolver: yupResolver(loginValidationSchema),
    });

    const onSubmit = (data) => {
        const { email, password } = data;
        alert(
            `Form data successfully validated.\n\nEmail: ${email} \nPassword: ${password}`
        );
        reset();
    };
    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            {/* JSX */}
        </form>
    );
};

export default ValidatingReactHookFormWithYup;

Conclusion

In conclusion, integrating Yup with React Hook Form brings together the best of both worlds, combining Yup's expressive schema-based validation approach with React Hook Form's performance-optimized, minimalist form management.

While the latter simplifies form state management and reduces boilerplate, the former offers a powerful syntax for defining intricate and reusable validation schemas that expressively describe data validation requirements, from basic presence checks to complex dependency rules.

This allows for cleaner, more maintainable code, where validation logic is decoupled from UI logic, leading to an improved development experience and more reliable applications.

Moreover, the combination of Yup's detailed validation error messages and React Hook Form's efficient handling of form states and errors allows developers to provide immediate, contextual feedback to users, significantly enhancing the user experience. Forms are more intuitive and interactive, guiding users through input requirements and errors with precision and clarity.

I hope you enjoyed this article and will tap into the power of Yup for form validation in your next React project. Feel free to like and leave a comment below.

Happy coding ๐Ÿ˜Š.

ย