DEV Community

Babatunde Adeniran
Babatunde Adeniran

Posted on

Formik & React (Part 3): Validation with yup & Streamlining Code

So far, we have managed the state of our order submission form, validated our form fields, displayed error messages,handled form submission, and tracked visited fields. However, there is an even better way to carry out form validation, and that is through the use of a library known as yup. With yup, we can eliminate manual interference from our validate function.

Validation with yup

We start by installing yup and importing it into our OrdersForm component. yup allows us to define an object schema validation that can be assigned to a yup object schema. This object will contain the validation rules for our form fields (name, email, and quantity).

import "./order.css"
import { useFormik } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}


const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})
Enter fullscreen mode Exit fullscreen mode

After this, the schema can be fed into the useFormik hook replacing the validate function that we previously used.

 const formik = useFormik({
    initialValues,
    onSubmit,
    validationSchema
  })
Enter fullscreen mode Exit fullscreen mode

So, we can remove our custom validation and our updated code becomes:

import "./order.css"
import { useFormik } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}


const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})


export const OrdersForm = () => {
  const formik = useFormik({
    initialValues,
    onSubmit,
    validationSchema
  })


  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form onSubmit={formik.handleSubmit}>
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.name}
          />
          {formik.touched.name && formik.errors.name ? (
            <div className="error">{formik.errors.name}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email}
          />
          {formik.touched.email && formik.errors.email ? (
            <div className="error">{formik.errors.email}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.quantity}
          />
          {formik.touched.quantity && formik.errors.quantity ? (
            <div className="error">{formik.errors.quantity}</div>
          ) : null}
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

Condensing logic and Refactoring for clarity

Although our form works as it should at the moment, it is possible to optimize our code structure and minimize redundancy. If we closely examine our name, email, and quantity form fields, we see that several props (onChange, onBlur, and value) are being repeated. This repetition violates the DRY (Don't Repeat Yourself) principle. To improve efficiency, we can abstract the common configuration from each field in our form.

A more streamlined approach is to use a helper method that dynamically applies these props as needed. Formik provides the formik.getFieldProps() method, which simplifies this process by accepting the field's name as its argument and automatically handling its state and events.

import "./order.css"
import { useFormik } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}

const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})


export const OrdersForm = () => {
  const formik = useFormik({
    initialValues,
    onSubmit,
    validationSchema
  })

  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form onSubmit={formik.handleSubmit}>
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            {...formik.getFieldProps('name')}
          />
          {formik.touched.name && formik.errors.name ? (
            <div className="error">{formik.errors.name}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            {...formik.getFieldProps('email')}

          />
          {formik.touched.email && formik.errors.email ? (
            <div className="error">{formik.errors.email}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            {...formik.getFieldProps('quantity')}
          />
          {formik.touched.quantity && formik.errors.quantity ? (
            <div className="error">{formik.errors.quantity}</div>
          ) : null}
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

So, three lines of code in each form field is replaced with a single line.

While the formik.getFieldProps() helper method helps reduce redundant code, we still need to manually pass it to each input field. This means that if our form has ten fields, we would have to call formik.getFieldProps() ten times, which can become repetitive.

To simplify this further, Formik offers built-in components that help reduce verbosity and improve readability. These include the Formik, Form, Field, and ErrorMessage components, which streamline form handling and validation.

The Formik component can serve as a replacement for the useFormik hook in our code. Previously, we passed initialValues, onSubmit, and validationSchema as an object argument within the useFormik hook. Now, instead of using useFormik, we will pass these configurations as props directly to the Formik component.

Before making this change, we need to update our imports by replacing useFormik with Formik.

import "./order.css"
import { Formik } from "formik"
import * as Yup from "yup"
Enter fullscreen mode Exit fullscreen mode

Next, we remove the useFormik call and wrap our entire form with the Formik component, passing the necessary props directly to it.

import "./order.css"
import { Formik } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}

const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})


export const OrdersForm = () => {
  return (
    <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
      <div className="form-container">
        <h2>Order Submission Form</h2>
        <form onSubmit={formik.handleSubmit}>
          <div className="form-group">
            <label htmlFor='name'>Name:</label>
            <input
              type="text"
              name="name"
              id='name'
              {...formik.getFieldProps('name')}
            />
            {formik.touched.name && formik.errors.name ? (
              <div className="error">{formik.errors.name}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='email'>Email:</label>
            <input
              type="email"
              name="email"
              id='email'
              {...formik.getFieldProps('email')}

            />
            {formik.touched.email && formik.errors.email ? (
              <div className="error">{formik.errors.email}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='quantity'>Order Quantity:</label>
            <input
              type="number"
              name="quantity"
              id='quantity'
              required
              {...formik.getFieldProps('quantity')}
            />
            {formik.touched.quantity && formik.errors.quantity ? (
              <div className="error">{formik.errors.quantity}</div>
            ) : null}
          </div>

          <button type="submit" className="submit-btn">
            Submit Order
          </button>
        </form>
      </div>
    </Formik>
  )
}

Enter fullscreen mode Exit fullscreen mode

We need to wrap the entire form with the Formik component to enable the use of additional components that simplify our form code. This allows us to introduce the Form component along with other necessary Formik components.

Next, we import the Form component and replace the standard HTML <form> element with it. Additionally, we remove the onSubmit prop from the Form tag. Internally, the Form component acts as a wrapper around the native <form> element, automatically integrating with Formik’s handleSubmit method.

import "./order.css"
import { Formik, Form } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}

const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})


export const OrdersForm = () => {
  return (
    <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
      <div className="form-container">
        <h2>Order Submission Form</h2>
        <Form>
          <div className="form-group">
            <label htmlFor='name'>Name:</label>
            <input
              type="text"
              name="name"
              id='name'
              {...formik.getFieldProps('name')}
            />
            {formik.touched.name && formik.errors.name ? (
              <div className="error">{formik.errors.name}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='email'>Email:</label>
            <input
              type="email"
              name="email"
              id='email'
              {...formik.getFieldProps('email')}

            />
            {formik.touched.email && formik.errors.email ? (
              <div className="error">{formik.errors.email}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='quantity'>Order Quantity:</label>
            <input
              type="number"
              name="quantity"
              id='quantity'
              required
              {...formik.getFieldProps('quantity')}
            />
            {formik.touched.quantity && formik.errors.quantity ? (
              <div className="error">{formik.errors.quantity}</div>
            ) : null}
          </div>

          <button type="submit" className="submit-btn">
            Submit Order
          </button>
        </Form>
      </div>
    </Formik>
  )
}

Enter fullscreen mode Exit fullscreen mode

Next, we introduce the Field component to simplify form field handling. Currently, we are using the getFieldProps helper method for each field, passing its corresponding name as an argument. However, we can further abstract this process to make our code cleaner and more efficient.

To achieve this, we import Field from Formik and replace our existing <input> elements with the Field component. This allows us to remove the getFieldProps helper method from each field, streamlining our form implementation. Our updated code now looks like this:

import "./order.css"
import { Formik, Form, Field } from "formik"
import * as Yup from "yup"


const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values)
}

const validationSchema = Yup.object({
  name: Yup.string().required("Name is required"),
  email: Yup.string().email("Invalid email format").required("Email is required"),
  quantity: Yup.number().required("Quantity is required")
})


export const OrdersForm = () => {
  return (
    <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
      <div className="form-container">
        <h2>Order Submission Form</h2>
        <Form>
          <div className="form-group">
            <label htmlFor='name'>Name:</label>
            <Field
              type="text"
              name="name"
              id='name'
            />
            {formik.touched.name && formik.errors.name ? (
              <div className="error">{formik.errors.name}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='email'>Email:</label>
            <Field
              type="email"
              name="email"
              id='email'
            />
            {formik.touched.email && formik.errors.email ? (
              <div className="error">{formik.errors.email}</div>
            ) : null}
          </div>

          <div className="form-group">
            <label htmlFor='quantity'>Order Quantity:</label>
            <Field
              type="number"
              name="quantity"
              id='quantity'
              required
            />
            {formik.touched.quantity && formik.errors.quantity ? (
              <div className="error">{formik.errors.quantity}</div>
            ) : null}
          </div>

          <button type="submit" className="submit-btn">
            Submit Order
          </button>
        </Form>
      </div>
    </Formik>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our form errors are still referencing formik, which was previously invoked through the useFormik hook that we have now removed. To address this, we will use the ErrorMessage component.

Currently, we are manually checking whether a field has been visited and whether an error exists before displaying the error message. Since we repeat this process for all three form fields, it introduces unnecessary redundancy. The ErrorMessage component helps eliminate this repetition.

As with the other Formik components, we first import ErrorMessage from Formik. Then, we replace our existing error messages with the ErrorMessage component, passing a name prop that matches the corresponding Field component’s name attribute.

We've successfully reduced the amount of code significantly and introduced yup for creating a validation schema. In the next article, we'll explore how to disable the submit button, load saved data, and reset the form data.

Top comments (0)