Mastering Form Validation in React with Formik and Yup (with TypeScript)

Oladipupo Ishola
12 min readJul 22, 2023

--

Introduction

Form validation is an important part of developing robust and user-friendly web applications. In React, developers frequently rely on powerful third-party libraries to simplify validation and provide a consistent user experience. Formik and Yup, two popular libraries, work in perfect harmony to provide an elegant and efficient solution for form validation in React. In this comprehensive guide, we will delve into the process of implementing form validation with Formik and Yup while leveraging TypeScript's additional benefits.

Why do I need another library?

While it is true that you can write your own form validation logic without relying on third-party libraries like Formik and Yup, consider the following form:

Design by Erfan | Ernad Studio

You practically have to handle the validation of every input field, state, error handling, error message, and so on. Sounds tedious, does not it? Do not worry, you are not required to respond; I have been there and understand. Do not get me wrong, it is doable, but why put yourself through all that stress when there is a tool available that can do the majority of the work?

Using these libraries can provide substantial benefits while saving you time and effort. Here are a few reasons why you should use Formik and Yup rather than write your own validation:

  1. Simplicity and Convenience: Formik and Yup provide a simple and declarative way to handle form validation in React. They abstract away much of the boilerplate code and provide a streamlined API for managing form state, handling validation, and displaying validation errors. This can save you from having to write complex validation logic from scratch and make your code more maintainable and readable.
  2. Validation Schema Definition: Yup, specifically, it offers a powerful validation schema definition syntax. It allows you to easily define complex validation rules, such as string, number, date, and object validation. Yup comes with a plethora of built-in validation methods, including required fields, min/max length, pattern matching, and more. This eliminates the need to write all of the validation logic manually and makes your code more concise.
  3. Built-in Error Handling: Formik integrates seamlessly with Yup, enabling automatic error handling and displaying validation errors in your form. With Formik, you can easily access error messages for each field and display them to the user. It handles the validation logic behind the scenes and provides a clean API for working with form errors.
  4. Form State Management: Formik takes care of managing form state, including field values, touched and dirty states, form submission, and resetting the form. It provides hooks and utilities for accessing and manipulating the form state, simplifying the overall form management process. This is especially useful when dealing with complicated forms with multiple fields and validation requirements.
  5. Community Support and Ecosystem: Formik and Yup are widely adopted libraries in the React community. Because these libraries have a large user base, you can find extensive documentation, tutorials, and community support when working with them. Furthermore, Formik has a robust ecosystem of extensions, plugins, and integrations to enhance your form development experience.

While it is possible to write your own form validation logic, leveraging the capabilities of Formik and Yup can save you time, improve code maintainability, and provide a more robust and user-friendly form validation solution. These libraries have been thoroughly tested and optimized for handling complex validation scenarios, allowing you to concentrate on developing your application's core features.

For the demo, we will build a basic registration form with validations and basic CSS styling (see the image below) nothing too fancy.

Without wasting much time, let’s get to work.

Prerequisites

Before we dive into the implementation, let’s ensure we have everything set up correctly. To follow along with this tutorial, you’ll need the following:

  1. Basic knowledge of React and TypeScript.
  2. A working React project with TypeScript support.
  3. Installation of required dependencies:
  • React: ^16.8.0 or later
  • Formik: ^2.2.0 or later
  • Yup: ^0.32.1 or later

Setting up the Project

To begin, let’s create a new React project and install the necessary dependencies. Open your terminal and execute the following commands:

npx create-react-app form-validation-demo --template typescript
cd form-validation-demo
npm install formik yup

This will create a new React project with TypeScript support and install the Formik and Yup libraries.

Creating a Form Component

In this section, we will create a form component that handles form input and validation. Create a new file called Form.tsx in the src folder and add the following code:

import React from 'react';

interface FormValues {
name: string;
email: string;
password: string;
confirmPassword: string;
}

const Form: React.FC = () => {
const initialValues: FormValues = {
name: '',
email: '',
password: '',
confirmPassword: '',
};

return (
<div>
<h1>Form Validation Example</h1>
<form>
{/* Form fields */}
</form>
</div>
);
};

export default Form;

Defining the Validation Schema with Yup

In this section, we will use Yup to define the validation schema for our form. Yup offers a powerful and expressive API for defining validation rules for different form fields.

To define the validation schema, import the necessary dependencies at the top of the Form.tsx file:

import { Formik, Field, ErrorMessage, FormikValues, FormikHelpers } from 'formik';
import * as Yup from 'yup';

Next, within the Form component, define the validation schema using Yup's object().shape() method:

const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
password: Yup.string()
.min(6, 'Password must be at least 6 characters')
.required('Password is required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Confirm Password is required'),
});

Here, we define validation rules for each form field. For example, the name field is required, the email field must be a valid email address, the password field must be at least 6 characters long; and the confirmPassword field must match the password field.

Within the Form component, wrap the form with the Formik component:

return (
<div>
<h1>Form Validation Example</h1>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<form>
{/* Form fields */}
</form>
</Formik>
</div>
);

Here, we pass the initialValues, validationSchema, and onSubmit handler to the Formik component. The initialValues define the initial state of the form fields, the validationSchema specifies the validation rules, and the onSubmit handler is invoked when the form is submitted.

Displaying Validation Errors

We will improve our form in this section by displaying validation errors for each field.

Within the form element, add the following code for each form field:


<label htmlFor="name">
Name:
<Field type="text" name="name" />
<ErrorMessage name="name" component="div" className="error" />
</label>

Here, we use the Field component to render the input field and bind it to the corresponding name field in the initialValues. The ErrorMessage component displays the validation error message for the specified field if it fails validation.

To style the error messages, let’s create a new file called Form.css in the src folder and add the following CSS code:

.input-err {
border: 1px solid red;
}

.error {
color: red;
font-size: 0.8rem;
margin-top: 0.2rem;
}

The input-err class add a border of red to the input box when the validation failed and error to display an error message.

Finally, import the CSS file into the Form.tsx file:

import './Form.css';

Handling Form Submission

Once the form is validated and the submit button is clicked, you’ll need to handle the form submission. Formik provides an onSubmit handler that receives the form values and formik helpers to perform actions after successful form submission.

In the Form component, define the handleSubmit function to handle the form submission:

const handleSubmit = (
values: FormikValues,
{ setSubmitting }: FormikHelpers<FormValues>
) => {
setTimeout(() => {
console.log(values);
setSubmitting(false);
}, 500);
};

In this example, the handleSubmit function logs the form values to the console after a short delay of 500ms. The setSubmitting function is used to indicate that the form submission is complete and enable the form for further interactions.

Below is the demo video of our form validation.

Form validation demo

Some Advanced Validation Techniques

Yup offers a wide range of advanced validation techniques that can be incorporated into your form validation process in addition to basic validation rules. Conditional validation, validating arrays, and composing multiple validation rules are some commonly used techniques.

To implement conditional validation, you can use Yup’s when method. You can use this method to define validation rules based on the values of other fields. For example, let's say you have a field called newsletter that should only be required if the user selects a specific option. You can define this rule as follows:

validationSchema = Yup.object().shape({
// other field validations...
newsletter: Yup.boolean().when('subscribe', {
is: true,
then: Yup.boolean().oneOf([true], 'You must subscribe to the newsletter'),
}),
});

In this example, the newsletter field will only be required if the subscribe field is set to true.

Custom Validation Functions

While Yup comes with a variety of built-in validation methods, there may be times when you need to implement custom validation logic. To handle complex validation requirements, Yup allows you to define custom validation functions.

To implement a custom validation function, you can use the test method provided by Yup. This method takes a string identifier and a callback function that performs the validation. For example, let's say you have a field called age that should only accept values between 18 and 65. You can define a custom validation rule as follows:

validationSchema = Yup.object().shape({
// other field validations...
age: Yup.number().test('age', 'Invalid age', (value) => {
return value >= 18 && value <= 65;
}),
});

In this example, the custom validation function checks if the age field value is between 18 and 65.

Asynchronous Validation

Asynchronous validation, such as validating against a server endpoint or performing complex asynchronous operations, may be required in some cases. Yup allows you to handle asynchronous validation using the test method with an asynchronous callback function.

To implement asynchronous validation, you can define an asynchronous callback function and use the resolve and reject parameters to handle the validation result. For example, let's say you have a field called username that needs to be unique and requires an asynchronous validation call to the server. You can define an asynchronous validation rule as follows:

validationSchema = Yup.object().shape({
// other field validations...
username: Yup.string().test('username', 'Username already exists', async (value) => {
return await isUsernameAvailable(value);
}),
});

In this example, the isUsernameAvailable function is an asynchronous function that checks if the username is available. The custom validation function awaits the asynchronous call and returns true or false based on the result.

Disabling the Submit Button

In some cases, you may want to disable the submit button until all form fields are valid. Formik provides a isSubmitting property that can be used to disable the submit button during form submission.

To disable the submit button until all form fields are valid, you can add the isSubmitting property to the Formik component and use it to conditionally disable the button:

<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, handleSubmit }) => (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={isSubmitting}>Submit</button>
</form>
)}
</Formik>

In this example, the submit button will be disabled when the form is being submitted (isSubmitting is true), ensuring that the form is not submitted multiple times.

Here is the entire code.

import React from 'react';
import { Formik, Field, ErrorMessage, FormikValues, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import './Form.css';

interface FormValues {
name: string;
email: string;
password: string;
confirmPassword: string;
}

const validationSchema = Yup.object().shape({
name: Yup.string()
.min(2, 'Name must be minimum 2')
.max(100, 'Name must not be more than 100 characters')
.required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
password: Yup.string()
.min(6, 'Password must be at least 6 characters')
.required('Password is required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Confirm Password is required'),
});

const Form: React.FC = () => {
const initialValues: FormValues = {
name: '',
email: '',
password: '',
confirmPassword: '',
};

const handleSubmit = (values: FormikValues,
{ setSubmitting }: FormikHelpers<FormValues>) => {
setTimeout(() => {
console.log(values);
setSubmitting(false);
}, 500);
}

return (
<div className='main'>
<h1>Registration Form</h1>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, errors, handleSubmit }) => (
<form onSubmit={handleSubmit}>

<label htmlFor="name">
Name:
<Field name="name" type="text" className={errors.name ? 'error' : ''}/>
<ErrorMessage className='error' name="name" component="div" />

</label>
<label htmlFor="email">
Email:
<Field name="email" type="email" className={errors.email ? 'error' : ''}/>
<ErrorMessage className='error' name="email" component="div" />
</label>

<label htmlFor="password">
Password:
<Field name="password" type="password" className={errors.password ? 'error' : ''}/>
<ErrorMessage className='error' name="password" component="div" />
</label>

<label htmlFor="confirmPassword">
Confirm Password:
<Field name="confirmPassword" type="password" className={errors.confirmPassword ? 'error' : ''}/>
<ErrorMessage className='error' name="confirmPassword" component="div" />
</label>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
</Formik>
</div>
);
};

export default Form;

If you prefer not to use the Formik component for form handling, you can still implement form validation with Yup in React without relying on wrapping the form with Formik as implemented above. Instead, you can directly use the Yup validation schema anduseFormik hook from Formik to handle form submissions using React’s built-in form handling and state management capabilities.

Here’s an example code snippet demonstrating how to implement form validation with Yup in React and the useFormik hook:

import React, { useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

interface FormValues {
name: string;
email: string;
password: string;
confirmPassword: string;
}

const Form: React.FC = () => {
const [submitting, setSubmitting] = useState(false);

const validationSchema = Yup.object().shape({
name: Yup.string()
.min(2, 'Name must be minimum 2')
.max(100, 'Name must not be more than 100 characters')
.required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
password: Yup.string()
.min(6, 'Password must be at least 6 characters')
.required('Password is required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Confirm Password is required'),
});

const handleSubmit = async (values: FormValues) => {
try {
setSubmitting(true);
// Perform form submission logic here
console.log(values);
// Set submitting to false after successful submission
setSubmitting(false);
} catch (error) {
// Handle form submission error
console.error(error);
setSubmitting(false);
}
};

const formik = useFormik({
initialValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
validationSchema,
onSubmit: handleSubmit,
});

return (
<div>
<h1>Registration Form</h1>
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
/>
{formik.touched.name && formik.errors.name && (
<div className="error">{formik.errors.name}</div>
)}
</div>

<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formik.values.email}
onChange={formik.handleChange}
/>
{formik.touched.email && formik.errors.email && (
<div className="error">{formik.errors.email}</div>
)}
</div>

<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
/>
{formik.touched.password && formik.errors.password && (
<div className="error">{formik.errors.password}</div>
)}
</div>

<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formik.values.confirmPassword}
onChange={formik.handleChange}
/>
{formik.touched.confirmPassword && formik.errors.confirmPassword && (
<div className="error">{formik.errors.confirmPassword}</div>
)}
</div>

<button type="submit" disabled={submitting}>
Submit
</button>
</form>
</div>
);
};

export default Form;

In this code example, we use the useFormik hook from Formik to handle form state management and validation. We define the validation schema using Yup and handle form submission using the onSubmit function. The form inputs are controlled using the value and onChange properties, and validation errors are displayed based on the formik touched and errors properties.

By using this approach, you have the flexibility to handle form validation and submission in a way that suits your requirements without relying on the Formik component.

In case you prefer a reusable custom hook that you can use across the entire codebase and in other future projects, see the link below.

Conclusion

In this comprehensive guide, we looked at how to use Formik and Yup to implement form validation in React while leveraging the power of TypeScript. We covered project setup, form component creation, defining validation schemas with Yup, integrating Formik and Yup, displaying validation errors, and implementing advanced validation techniques.

By combining the capabilities of Formik and Yup, you can create highly sophisticated and user-friendly forms with seamless validation experiences. Remember to customize the validation rules to your specific form requirements and to take advantage of advanced techniques such as conditional validation, custom validation functions, asynchronous validation, and disabling the submit button.

With the knowledge gained from this guide, you are well-equipped to handle form validation challenges in your React applications. Form validation in TypeScript with Formik and Yup not only improves your development workflow but also ensures your forms are robust, secure, and user-friendly.

You can read about using React Hook Form with Zod, another powerful library for form validation, in my article below.

Now, go ahead and create amazing forms with confidence!

The demo’s GitHub repository is available here, where you can view the entire code as well as the CSS stylings.

Happy coding!

--

--

Oladipupo Ishola

Tech Writer | Full Stack Developer | Frontend Developer | Backend Developer | Co-Building @techverseacademy | Mentor & Code Instructor | MLH ’21 Alum