In our previous article, we introduced Formik and explored how the useFormik
hook simplifies form state management, form submission, and validation. However, there are still a few crucial improvements needed to enhance the user experience.
While we have implemented basic validation, there’s room for improvement. Currently, our form does not display error messages or keep track of fields that have already been visited—both of which are essential for a better user experience.
In this article, we’ll address these issues and take our form handling to the next level.
Let’s dive in!
Displaying Error Messages
We already have a validate
function that checks for errors in our form fields (name, email, and quantity) and returns corresponding error messages based on our validation rules and conditions. However, we still need a way to display these error messages to the user.
As we’ve established, the formik
object returned by the useFormik
hook contains various helper methods and useful properties for managing our form. So far, we’ve explored initialValues
, onSubmit
, and validate
.
The validate
function helps us retrieve error messages, but how do we access them? Recall that formik.values
contains key-value pairs for our name
, email
, and quantity
form fields. Similarly, Formik provides an errors
property, which follows the same structure as formik.values
—it’s an object with key-value pairs for our form fields, holding their corresponding validation errors.
Let’s log formik.errors
to the console and see what it contains.
const formik = useFormik({
initialValues,
onSubmit,
validate
})
console.log("Form errors", formik.errors)
Typically, formik.errors
returns an empty object when the page mounts.
However, when we alter the initialValues
, the errors
object is populated with keys that are similar to that of formik.values
from our previous article. We would also see that the value of each error field is the error message we defined in our validate
function.
What Formik does under the hood is to run the validate
function when the form field changes. The formik.errors
object is then populated with error messages based on the rules outlined in the validate
function.
Next, we conditionally render error messages from the formik.errors
object in JSX
.
import "./order.css"
import { useFormik } from "formik"
const initialValues = {
name: "",
email: "",
quantity: 0
}
const onSubmit = (values) => {
console.log(values)
}
const validate = (values) => {
const errors = {}
if (!values.name) {
errors.name = "Name is required"
}
if (!values.email) {
errors.email = "Email is required"
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
) {
errors.email = "Invalid email address"
}
if (!values.quantity) {
errors.quantity = "Quantity is required"
}
return errors
}
export const OrdersForm = () => {
const formik = useFormik({
initialValues,
onSubmit,
validate
})
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}
value={formik.values.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}
value={formik.values.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}
value={formik.values.quantity}
/>
{formik.errors.quantity ? (
<div className="error">{formik.errors.quantity}</div>
) : null}
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
We would also add an error class
to our order.css
file.
.form-container {
width: 350px;
margin: 40px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
text-align: center;
}
h2 {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
text-align: left;
}
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit-btn {
width: 100%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submit-btn:hover {
background-color: #0056b3;
}
.error {
color: red;
margin-top: 5px;
}
Although the error messages are displayed correctly, there is still an issue to fix. Currently, error messages for all three fields appear even when we interact with just one input field. This is not an ideal user experience, let's correct it.
Visited Fields
Although the error messages are visible to users, we have to keep track of visited fields so that error messages are not displayed on inputs or fields that are yet to be visited. As it stands, every keyboard event triggers the validate
function to be called and this means the form.errors
object containing the error messages for name, email, and quantity will be populated at every instance. We only want to display error messages for the field the user has interacted with, and it even makes more sense to display error messages for a field after the user has finished typing in that field.
The onBlur
prop tracks which fields have been visited. Adding it to our input fields will resolve this issue. Formik also provides a helper method called handleBlur
, which we pass to the onBlur
prop.
Formik saves the details of already visited fields in the touched
object. The touched
object has a similar structure to the formik.errors
and formik.values
objects.
import "./order.css"
import { useFormik } from "formik"
const initialValues = {
name: "",
email: "",
quantity: 0
}
const onSubmit = (values) => {
console.log(values)
}
const validate = (values) => {
const errors = {}
if (!values.name) {
errors.name = "Name is required"
}
if (!values.email) {
errors.email = "Email is required"
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
) {
errors.email = "Invalid email address"
}
if (!values.quantity) {
errors.quantity = "Quantity is required"
}
return errors
}
export const OrdersForm = () => {
const formik = useFormik({
initialValues,
onSubmit,
validate
})
console.log("Visited form fields", formik.touched)
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.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.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.errors.quantity ? (
<div className="error">{formik.errors.quantity}</div>
) : null}
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
If we check the logs for the visited fields on page load, we should see an empty object indicating that none of the fields have been visited.
Let's click in and out of the email
field
We can see that the touched
object is no longer empty—the email
field has been marked as visited. The same applies to the name
and quantity
fields when touched. The details of the formik.touched
object can then be used to dynamically render error messages.
import "./order.css"
import { useFormik } from "formik"
const initialValues = {
name: "",
email: "",
quantity: 0
}
const onSubmit = (values) => {
console.log(values)
}
const validate = (values) => {
const errors = {}
if (!values.name) {
errors.name = "Name is required"
}
if (!values.email) {
errors.email = "Email is required"
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
) {
errors.email = "Invalid email address"
}
if (!values.quantity) {
errors.quantity = "Quantity is required"
}
return errors
}
export const OrdersForm = () => {
const formik = useFormik({
initialValues,
onSubmit,
validate
})
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>
)
}
There are other ways we can define our form validation rules. We will look at that next.
Top comments (0)