Skip to main content

Advanced Form Handling with Formik and Yup

Formik and Yup together provide a powerful pattern for building complex React forms. Formik's declarative components (<Formik />, <Form />, <Field />, <ErrorMessage />) handle state and submission logic, while Yup offers a clean, readable schema-based validation system. This combination eliminates boilerplate, keeps components focused, and makes validation rules easy to maintain and reuse.

Key Takeaways

  • Formik's declarative components simplify form state management compared to manual useState patterns
  • Yup validation schemas keep validation logic separate from component code and are highly reusable
  • The <Field /> component automatically syncs with Formik state, handling onChange, onBlur, and value
  • The <ErrorMessage /> component displays field errors only when needed, reducing conditional rendering code
  • Combining Formik and Yup eliminates boilerplate and creates a single, testable source of form validation rules

What Are Formik's Declarative Components?

Formik provides four core components that make form building more declarative and less verbose:

  • <Formik /> — Root component managing form state, validation, and submission.
  • <Form /> — Wrapper for the HTML <form> element; automatically hooks into Formik's handleSubmit.
  • <Field /> — Connects an input to Formik state; automatically handles onChange, onBlur, and value.
  • <ErrorMessage /> — Displays validation errors for a specific field only when the field is touched and invalid.

Using these components instead of manual onChange handlers and useState significantly reduces code complexity.

How Do You Create a Validation Schema with Yup?

Yup is a JavaScript schema validation library. It allows you to define your validation rules in a separate object, keeping your component code clean. Rules are chainable and self-documenting.

import * as Yup from 'yup';

export const validationSchema = Yup.object({
firstName: Yup.string()
.max(15, 'Must be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string()
.email('Invalid email address')
.required('Required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password'), null], 'Passwords must match')
.required('Required'),
});

Breakdown:

  • Yup.string() — Validates the field is a string.
  • .max(15, message) — Limits length to 15 characters; shows custom message if exceeded.
  • .email(message) — Validates email format using RFC 5322.
  • .min(8, message) — Enforces minimum length.
  • .oneOf([Yup.ref('password'), null], message) — Cross-field validation; confirmPassword must match password.
  • .required(message) — Field cannot be empty.

How Do You Build a Registration Form with Formik and Yup?

Here is a complete registration form using Formik's declarative components and Yup validation:

Installation:

npm install formik yup

Registration Form Component:

import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const validationSchema = Yup.object({
firstName: Yup.string()
.max(15, 'Must be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string()
.email('Invalid email address')
.required('Required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password'), null], 'Passwords must match')
.required('Required'),
});

function RegistrationForm() {
return (
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log('Form submitted:', values);
setSubmitting(false);
}, 400);
}}
>
<Form>
<div>
<label htmlFor="firstName">First Name</label>
<Field name="firstName" type="text" />
<ErrorMessage name="firstName" component="div" className="error" />
</div>

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

<div>
<label htmlFor="email">Email Address</label>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" className="error" />
</div>

<div>
<label htmlFor="password">Password</label>
<Field name="password" type="password" />
<ErrorMessage name="password" component="div" className="error" />
</div>

<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<Field name="confirmPassword" type="password" />
<ErrorMessage name="confirmPassword" component="div" className="error" />
</div>

<button type="submit">Submit</button>
</Form>
</Formik>
);
}

export default RegistrationForm;

Step-by-Step Breakdown:

  1. <Formik /> — Wraps the form and manages state. Props: initialValues (form field defaults), validationSchema (Yup rules), onSubmit (submission handler).
  2. <Form /> — Replaces HTML <form>; automatically connects to Formik's handleSubmit and prevents default submission.
  3. <Field /> — Each input automatically syncs with Formik state. No need for manual onChange or value props.
  4. <ErrorMessage /> — Displays the error for each field only when that field is touched and invalid.
  5. setSubmitting(false) — Called in onSubmit to clear the submitting state and re-enable the button.

Best Practices for Formik and Yup

  • Separate validation schema: Keep the Yup schema in its own file for reusability and testability.
  • Use <ErrorMessage /> with component: Render errors as div, span, or a custom component; avoids conditional rendering.
  • Leverage touched state: Formik tracks touched fields; <ErrorMessage /> shows errors only for touched fields by default.
  • Cross-field validation: Use Yup.ref() to validate one field against another (e.g., password confirmation).
  • Custom field components: Wrap <Field /> in your own input component for consistency and styling.
  • Async validation: Pass an validate function to <Formik /> for server-side validation (e.g., checking email uniqueness).

Frequently Asked Questions

What is the difference between <Field /> and a regular HTML <input />?

The <Field /> component automatically connects to Formik's state management. It handles onChange, onBlur, and value props behind the scenes. A regular <input /> would require manual event handlers and value syncing.

How do you customize the appearance of error messages?

Pass the component prop to <ErrorMessage />. You can render a custom component: <ErrorMessage name="email" component={CustomError} />. The custom component receives the children prop (error message text).

Can you validate a field against another field in Yup?

Yes, use Yup.ref(). For example, confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], 'Passwords must match') validates that confirmPassword matches password.

How do you submit the form programmatically from another component?

Use useFormikContext() inside a child component to access Formik's handleSubmit: const { handleSubmit } = useFormikContext(); handleSubmit();

What does setSubmitting(false) do in the onSubmit handler?

It marks the form as no longer submitting, re-enabling the submit button. Formik disables the button while isSubmitting is true to prevent duplicate submissions.

Further Reading