The importance of forms in business applications cannot be overemphasized. We use forms to accomplish many tasks and facilitate various processes, such as user registration, user sign-in, feedback collection, and order creation. Because forms are crucial to most web interactions, it is essential to ensure a seamless user experience through effective development.
When creating forms, developers take on numerous responsibilities, including field validation, efficient handling of form data, comprehensive error management, and an optimized form submission process.
In this article, I will explain how we can achieve all these with Formik, a highly useful library for managing forms in React and React Native.
Formik simplifies form handling by managing form data, validation, submission, and error messages efficiently. While React
provides multiple approaches to handling forms, Formik abstracts the complex and tedious parts, allowing developers to build advanced forms with ease. It is both scalable and performant, making it an excellent choice for form management in React applications.
We start with a simple presentational React.js
form component without any functionality. The goal is to implement form state management, handle form submission, validate inputs, and display error messages. The form is designed for submitting orders for an imaginary company, Love & Light Pizzas Ltd. The order submission form has three fields: name
, email
, and order quantity
. It will also include a submit button.
import "./order.css"
export const OrdersForm = () => {
return (
<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'
required
/>
</div>
<div className="form-group">
<label htmlFor='email'>Email:</label>
<input
type="email"
name="email"
id='email'
required
/>
</div>
<div className="form-group">
<label htmlFor='quantity'>Order Quantity:</label>
<input
type="number"
name="quantity"
id='quantity'
required
/>
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
.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;
}
After installing Formik, our next step is to import the useFormik
hook which the library provides. Since hooks are simply functions, they need to be called within the component. The useFormik hook returns an object containing a variety of methods and properties that can be used within the form. With this Formik object, it becomes easier to achieve our aim of handling form submission, managing the state of the form, and validating input error messages.
FORM STATE MANAGEMENT
We already know that state is an object that determines how a component renders and behaves. At the moment, our form component is an uncontrolled component, which means it is a traditional HTML form input whose value we cannot manage directly. Our order form has three fields, but as it stands, we are not keeping track of the form field values. When we enter something into the input fields, the field value changes. In React
, when there are changes in values, we use state variables to keep track of those changes. So, we need state variables for the name
, email
, and order quantity
inputs. Together, these are referred to as the form state.
In Formik, the form state is an object that maintains the value of individual form fields. Therefore, it is crucial to understand how Formik helps manage the form state when form field values change.
We first pass a property called initial values
in the object configuration. It is an object that contains the initial values of our form fields. The properties of the initialValues
object must correspond with the name attribute of our form inputs.
To track changes in individual form inputs, we add an onChange
and value
prop to each input. This is similar to basic form handling in React.js
, where we assign the state property as the input’s value and handle the onChange
event by creating an event handler. With Formik, we use formik.handleChange
to handle this event. Additionally, we can sync the value of the input with the state property by accessing formik.values.nameAttribute
, as shown below.
import "./order.css"
import { useFormik } from "formik"
export const OrdersForm = () => {
const formik = useFormik({
initialValues: {
name: "",
email: "",
quantity: 0
}
})
return (
<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'
required
onChange={formik.handleChange}
value={formik.values.name}
/>
</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}
/>
</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}
/>
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
When we log the formik.values
object in the console, we would see that it is structured such that the keys are the name attributes of the form fields, and as we type into individual fields, the changes are reflected in each input value.
We see that at every point in time, formik.values
reflects the application state.
HANDLING FORM SUBMISSION
So far, we have discussed how the form values are tracked and extracted. However, we have not yet covered how the values are handled when the user clicks the submit button. There are two steps involved in form submission with Formik.
The first step is to specify the onSubmit handler on the <form>
tag, just as we listened to change events on form inputs using the onChange
handler. Typically, the Formik helper method we previously defined provides a method for handling form submission, called the handleSubmit
method.
For the second step, we return to our useFormik
hook configuration and pass a second property called onSubmit
. By default, the onSubmit method receives the state of the form as its argument. The values
argument in the onSubmit
method is a reference to formik.values
. The onSubmit
method is triggered as soon as the submit button is clicked.
import "./order.css"
import { useFormik } from "formik"
export const OrdersForm = () => {
const formik = useFormik({
initialValues: {
name: "",
email: "",
quantity: 0
},
onSubmit: (values) => {
console.log(values, "Form data")
}
})
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}
/>
</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}
/>
</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}
/>
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
We see that the logs after the submit button is clicked are in sync with the content of the form.
HANDLING FORM VALIDATION
As it stands, we can manage the state of our order form and submit data by clicking a button. However, we have not yet validated our form fields to filter form values based on established rules.
With Formik, we can define a validation function assigned to the validate
property, which will be our third property in the useFormik
hook configuration. Like the onSubmit
property, the validate
function receives the values
object as an argument by default. There are certain conditions that the validate
function must satisfy, one of these is that it must return an object. We assign this object to a variable and return it.
validate: (values) => {
const errors: any = {}
return errors
}
Another condition that must be fulfilled is that the keys of the values
and errors
objects must be in sync. Currently, we have values.name
, values.email
, and values.quantity
. Similarly, we will have errors.name
, errors.email
, and errors.quantity
. Additionally, the values of the keys defined in the errors object must be strings that convey the correct error message.
import "./order.css"
import { useFormik } from "formik"
export const OrdersForm = () => {
const formik = useFormik({
initialValues: {
name: "",
email: "",
quantity: 0
},
onSubmit: (values) => {
console.log(values, "Form data")
},
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
}
})
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}
/>
</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}
/>
</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}
/>
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
Now, let's refactor this code to improve the readability of the userFormik
hook.
import "./order.css"
import { useFormik } from "formik"
const initialValues = {
name: "",
email: "",
quantity: 0
}
const onSubmit = (values) => {
console.log(values, "Form data")
}
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}
/>
</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}
/>
</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}
/>
</div>
<button type="submit" className="submit-btn">
Submit Order
</button>
</form>
</div>
)
}
In the next article, we will cover how to display these error messages, identify visited fields, define a validation schema with Yup
, and reduce boilerplate to improve reusability.
Top comments (0)