When working with React, forms are something you will deal with in almost every project.
Whether you're building a login page, registration form, admin dashboard, job application portal, e-commerce checkout, or a multi-step form, managing form data and validation can quickly become complicated.
Initially, handling forms using React's useState seems straightforward. But as the application grows, things get messy.
You start dealing with:
- Managing multiple input values.
- Handling validation for every field.
- Displaying error messages.
- Tracking touched and dirty fields.
- Managing form submission.
- Handling API errors.
- Preventing unnecessary re-renders.
- Supporting dynamic fields and complex validations.
This is where form management libraries become useful.
Two popular choices in the React ecosystem are React Hook Form and Formik.
I've worked with both approaches, and one thing I've noticed is that choosing a form library isn't just about writing fewer lines of code. It's also about understanding how it manages form state and how that affects your application.
In this article, we'll implement the same registration form using both libraries, understand their differences, and discuss when to choose each one.
Let's get started!
1. Understanding the Two Libraries
Before comparing them, let's understand their fundamental approaches.
What is Formik?
Formik is a React form management library that helps developers manage form values, validation, errors, touched fields, and submissions.
Formik follows a controlled component approach.
This means Formik manages the form values in its state. Whenever a user types something into an input, the corresponding form state gets updated.
For example:
const formik = useFormik({ initialValues: { email: "", password: "", }, onSubmit: (values) => { console.log(values); }, }); Formik provides useful handlers such as:
handleChangehandleBlurhandleSubmitsetFieldValuesetFieldTouchedsetFieldError
It also provides components such as Formik, Field, ErrorMessage, and FieldArray.
What is React Hook Form?
React Hook Form is a React library designed to manage forms with minimal re-renders and a lightweight API.
It primarily uses an uncontrolled component approach for native HTML inputs.
Instead of updating React state on every keystroke, it connects inputs to the form using refs and manages their values internally.
For example:
const { register, handleSubmit } = useForm({ defaultValues: { email: "", password: "", }, }); const onSubmit = (values) => { console.log(values); }; Some important methods include:
registerhandleSubmitwatchsetValueresetsetErroruseFieldArrayController
React Hook Form also supports controlled components when necessary.
2. Let's Build the Same Registration Form Using Both Libraries
Theory is good, but implementing the same functionality with both libraries will make the differences much clearer.
We'll build a registration form with the following fields:
- Full Name
- Password
- Confirm Password
Our requirements:
- All fields are required.
- Email must have a valid format.
- Password must contain at least 8 characters.
- Confirm Password must match Password.
- Display validation errors.
- Submit the form data.
Approach 1: Implementing the Form Using Formik
Step 1: Install the package
npm install formik Step 2: Create the Registration Component
import React from "react"; import { useFormik } from "formik"; const RegistrationForm = () => { const formik = useFormik({ initialValues: { fullName: "", email: "", password: "", confirmPassword: "", }, validate: (values) => { const errors = {}; if (!values.fullName.trim()) { errors.fullName = "Full name is required"; } if (!values.email) { errors.email = "Email is required"; } else if ( !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email) ) { errors.email = "Invalid email address"; } if (!values.password) { errors.password = "Password is required"; } else if (values.password.length < 8) { errors.password = "Password must contain at least 8 characters"; } if (!values.confirmPassword) { errors.confirmPassword = "Please confirm your password"; } else if ( values.confirmPassword !== values.password ) { errors.confirmPassword = "Passwords do not match"; } return errors; }, onSubmit: (values) => { console.log("Form Submitted:", values); }, }); return ( <form onSubmit={formik.handleSubmit}> <div> <label>Full Name</label> <input type="text" name="fullName" value={formik.values.fullName} onChange={formik.handleChange} onBlur={formik.handleBlur} /> {formik.touched.fullName && formik.errors.fullName && ( <p>{formik.errors.fullName}</p> )} </div> <div> <label>Email</label> <input type="email" name="email" value={formik.values.email} onChange={formik.handleChange} onBlur={formik.handleBlur} /> {formik.touched.email && formik.errors.email && ( <p>{formik.errors.email}</p> )} </div> <div> <label>Password</label> <input type="password" name="password" value={formik.values.password} onChange={formik.handleChange} onBlur={formik.handleBlur} /> {formik.touched.password && formik.errors.password && ( <p>{formik.errors.password}</p> )} </div> <div> <label>Confirm Password</label> <input type="password" name="confirmPassword" value={formik.values.confirmPassword} onChange={formik.handleChange} onBlur={formik.handleBlur} /> {formik.touched.confirmPassword && formik.errors.confirmPassword && ( <p>{formik.errors.confirmPassword}</p> )} </div> <button type="submit"> Register </button> </form> ); }; export default RegistrationForm; That's it!
Formik handles the form state and provides the necessary methods to update values, track user interactions, and validate the form.
But how does it actually work?
Let's understand some important properties:
| Property | Purpose |
|---|---|
initialValues | Defines initial form values |
values | Contains current form values |
handleChange | Updates the value when an input changes |
handleBlur | Tracks when a user leaves an input |
touched | Tracks fields the user has interacted with |
errors | Stores validation errors |
handleSubmit | Handles form submission |
One important thing to understand is that Formik keeps the form values in React-managed state.
When an input changes, Formik updates its state, which can trigger re-renders in components subscribed to its context.
This is an important consideration when working with large forms.
Approach 2: Implementing the Same Form Using React Hook Form
Now let's implement exactly the same functionality using React Hook Form.
Step 1: Install the package
npm install react-hook-form Step 2: Create the Registration Component
import React from "react"; import { useForm } from "react-hook-form"; const RegistrationForm = () => { const { register, handleSubmit, watch, formState: { errors }, } = useForm({ defaultValues: { fullName: "", email: "", password: "", confirmPassword: "", }, }); const password = watch("password"); const onSubmit = (values) => { console.log("Form Submitted:", values); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label>Full Name</label> <input type="text" {...register("fullName", { required: "Full name is required", })} /> {errors.fullName && ( <p>{errors.fullName.message}</p> )} </div> <div> <label>Email</label> <input type="email" {...register("email", { required: "Email is required", pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email address", }, })} /> {errors.email && ( <p>{errors.email.message}</p> )} </div> <div> <label>Password</label> <input type="password" {...register("password", { required: "Password is required", minLength: { value: 8, message: "Password must contain at least 8 characters", }, })} /> {errors.password && ( <p>{errors.password.message}</p> )} </div> <div> <label>Confirm Password</label> <input type="password" {...register("confirmPassword", { required: "Please confirm your password", validate: (value) => value === password || "Passwords do not match", })} /> {errors.confirmPassword && ( <p>{errors.confirmPassword.message}</p> )} </div> <button type="submit"> Register </button> </form> ); }; export default RegistrationForm; Notice something?
We didn't write separate onChange, onBlur, or value handlers for every input.
That's because register() handles the connection between the input and React Hook Form.
Let's understand the important methods:
| Method / Property | Purpose |
|---|---|
register() | Registers an input with the form |
handleSubmit() | Validates and handles form submission |
watch() | Watches the value of a field |
setValue() | Updates a field programmatically |
reset() | Resets the form |
errors | Contains validation errors |
defaultValues | Defines initial form values |
One thing worth highlighting is that register() returns the necessary props, including name, ref, onChange, and onBlur.
This makes it particularly convenient when working with native HTML inputs.
3. The Biggest Difference: Controlled vs Uncontrolled Components
This is probably the most important concept to understand when comparing these two libraries.
Let's break it down.
Controlled Components
In controlled components, React manages the input value through state.
Example:
const [email, setEmail] = useState(""); <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> Here:
- React state stores the input value.
- The user types something.
- The
onChangeevent updates the state. - React renders the component with the updated value.
Formik follows this approach for managing form values.
Uncontrolled Components
In uncontrolled components, the DOM maintains the current input value.
Example:
const inputRef = useRef(null); <input type="email" ref={inputRef} /> We can access the input value using:
console.log(inputRef.current.value); React Hook Form primarily uses this approach with native inputs.
It uses refs to connect inputs to its internal form management system.
The result?
It can avoid triggering a React render for every input change in the same way a fully controlled form typically does.
Important: React Hook Form also supports controlled components through Controller and useController. So, it's not limited to uncontrolled inputs.
4. Performance: Why Does React Hook Form Have an Advantage?
Let's consider a practical example.
Imagine you're building an employee management dashboard with a form containing 30-40 fields.
Each field has:
- Input values
- Validation rules
- Error messages
- Conditional fields
- Dynamic dropdowns
With a controlled form approach, updating form values can cause subscribed React components to re-render as state changes.
React Hook Form takes a different approach.
It minimizes unnecessary re-renders by managing form values outside the typical React component state update cycle and allowing components to subscribe to specific form state.
For example:
const { register, formState: { errors }, } = useForm(); React Hook Form also provides useWatch and useFormState to subscribe to specific values or state updates.
This can be particularly useful when building large forms.
However, don't misunderstand this:
Formik is not automatically slow, and React Hook Form is not automatically fast in every situation.
Performance depends on component structure, validation complexity, subscriptions, and how the form is implemented.
For a small login form, you might never notice a meaningful difference.
For a large dynamic form, React Hook Form's approach can offer a noticeable advantage.
5. Validation: How Do Both Libraries Handle It?
Validation is one of the most important parts of form management.
Both libraries support:
- Required field validation
- Minimum and maximum length
- Email validation
- Custom validation
- Cross-field validation
- Schema-based validation
Let's understand how they handle schema validation.
Formik with Yup
Formik has built-in support for Yup integration.
First, install Yup:
npm install yup Create a validation schema:
import * as Yup from "yup"; const validationSchema = Yup.object({ fullName: Yup.string() .trim() .required("Full name is required"), email: Yup.string() .email("Invalid email address") .required("Email is required"), password: Yup.string() .min(8, "Minimum 8 characters required") .required("Password is required"), confirmPassword: Yup.string() .oneOf( [Yup.ref("password")], "Passwords do not match" ) .required("Please confirm your password"), }); Now pass this schema to Formik:
const formik = useFormik({ initialValues: { fullName: "", email: "", password: "", confirmPassword: "", }, validationSchema, onSubmit: (values) => { console.log(values); }, }); Formik automatically uses the schema to validate the form.
React Hook Form with Yup
React Hook Form supports external validation libraries through resolvers.
Install the required packages:
npm install yup @hookform/resolvers Create the same Yup schema:
import * as Yup from "yup"; const validationSchema = Yup.object({ fullName: Yup.string() .trim() .required("Full name is required"), email: Yup.string() .email("Invalid email address") .required("Email is required"), password: Yup.string() .min(8, "Minimum 8 characters required") .required("Password is required"), confirmPassword: Yup.string() .oneOf( [Yup.ref("password")], "Passwords do not match" ) .required("Please confirm your password"), }); Connect it with React Hook Form:
import { useForm } fromSource:https://dev.to/sajal_sahu/formik-vs-react-hook-form-which-one-should-you-choose-for-your-react-projects-3afm