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")
})
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
})
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>
)
}
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>
)
}
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"
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>
)
}
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>
)
}
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>
)
}
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)