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 } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(validationSchema),
});
That's all!
Both libraries can use the same Yup schema, so you don't need to rewrite your validation logic when switching between them.
Another useful option is Zod, which is particularly popular in TypeScript projects.
6. Handling API Errors and Form Submission
In real-world applications, validating fields on the frontend is only one part of the process.
We also need to communicate with backend APIs.
For example, imagine a registration API returns an error because the email address already exists.
Let's see how both libraries handle this scenario.
Formik: Setting Server-Side Errors
Formik provides setFieldError() and setErrors().
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: async (values, { setFieldError }) => {
try {
const response = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
const data = await response.json();
if (!response.ok) {
if (data.field === "email") {
setFieldError("email", data.message);
}
return;
}
console.log("Registration successful");
} catch (error) {
console.error("Something went wrong", error);
}
},
});
React Hook Form: Setting Server-Side Errors
React Hook Form provides setError() for handling server-side validation errors.
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm();
const onSubmit = async (values) => {
try {
const response = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
const data = await response.json();
if (!response.ok) {
if (data.field === "email") {
setError("email", {
type: "server",
message: data.message,
});
}
return;
}
console.log("Registration successful");
} catch (error) {
setError("root.server", {
type: "server",
message: "Something went wrong. Please try again.",
});
}
};
You can display the server error like this:
{errors.email && (
<p>{errors.email.message}</p>
)}
{errors.root?.server && (
<p>{errors.root.server.message}</p>
)}
And disable the submit button while the request is running:
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Register"}
</button>
Both libraries provide enough flexibility to handle real-world API integrations.
7. Working with Dynamic Fields
Imagine you're building a job application form.
An applicant can add multiple educational qualifications:
- Graduation
- Post Graduation
- Diploma
- Other qualifications
The number of fields depends on the user's actions.
This is where dynamic field management becomes important.
Formik provides FieldArray, while React Hook Form provides useFieldArray.
Formik Example
import { FieldArray, Formik } from "formik";
<Formik
initialValues={{
skills: [""],
}}
onSubmit={(values) => console.log(values)}
>
{({ values, handleChange, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<FieldArray name="skills">
{({ push, remove }) => (
<>
{values.skills.map((skill, index) => (
<div key={index}>
<input
name={`skills.${index}`}
value={skill}
onChange={handleChange}
/>
<button
type="button"
onClick={() => remove(index)}
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => push("")}
>
Add Skill
</button>
</>
)}
</FieldArray>
<button type="submit">Submit</button>
</form>
)}
</Formik>
React Hook Form Example
import { useForm, useFieldArray } from "react-hook-form";
const DynamicSkillsForm = () => {
const { register, control, handleSubmit } = useForm({
defaultValues: {
skills: [{ name: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "skills",
});
const onSubmit = (values) => {
console.log(values);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`skills.${index}.name`)}
/>
<button
type="button"
onClick={() => remove(index)}
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "" })}
>
Add Skill
</button>
<button type="submit">Submit</button>
</form>
);
};
export default DynamicSkillsForm;
Both approaches support dynamic fields, but React Hook Form provides a particularly convenient API for managing field arrays.
8. React Hook Form vs Formik: Side-by-Side Comparison
Now that we've implemented both libraries, let's compare their important characteristics.
| Feature | Formik | React Hook Form |
|---|---|---|
| Core approach | Controlled | Primarily uncontrolled |
| Form state | React state and context | Internal form control and subscriptions |
| Re-renders | Can re-render subscribed components on state changes | Optimized to minimize unnecessary re-renders |
| Validation | Built-in and Yup integration | Built-in and resolver integration |
| Custom validation | Supported | Supported |
| Schema validation | Yup, etc. | Yup, Zod, etc. |
| Dynamic fields | FieldArray | useFieldArray |
| API error handling | setFieldError, setErrors | setError |
| Custom components | Supported | Controller / useController |
| TypeScript | Supported | Strong TypeScript support |
| Form reset | resetForm | reset |
Which One Is Easier to Learn?
Formik can feel more straightforward if you're already comfortable with controlled React components and React state.
React Hook Form has a slightly different learning curve because you need to understand concepts like:
- Registering inputs
- Uncontrolled components
- Form subscriptions
- Controller
- Watch
Once you understand these concepts, however, its API becomes quite intuitive.
9. When Should You Choose Formik?
Formik is still a useful option, especially when:
- Your existing application already uses Formik.
- Your team is familiar with its API.
- You're working with controlled components.
- You have an existing Formik-based form architecture.
- You want its component-based API and familiar state management approach.
There is no reason to migrate a stable Formik application just because another library is available.
10. When Should You Choose React Hook Form?
I would consider React Hook Form when:
- You're starting a new React application.
- Your application contains large or complex forms.
- You want to minimize unnecessary re-renders.
- You're working with TypeScript.
- You're building dynamic forms.
- You're using UI libraries such as Material UI, Ant Design, or custom input components.
- You want flexible validation through Yup or Zod.
For new projects, React Hook Form would generally be my first choice.
11. My Take: Which One Would I Choose?
After understanding both libraries, I think the decision becomes much easier.
If I were building a new MERN application today, I would lean towards React Hook Form.
Why?
Because in a real-world application, forms are rarely limited to four or five input fields.
Consider a recruitment management system.
You might have forms for:
- Creating job postings.
- Managing candidate information.
- Updating recruiter profiles.
- Configuring subscription plans.
- Managing company details.
- Handling job applications.
Some of these forms can contain dozens of fields, conditional validations, dynamic inputs, and API integrations.
In such situations, React Hook Form offers a convenient API and an architecture designed to minimize unnecessary re-renders.
But I wouldn't say Formik is a bad choice.
If your existing project uses Formik and performs well, continue using it. Switching libraries without a real requirement can introduce unnecessary development and maintenance costs.
The best library isn't necessarily the one with the most features. It's the one that solves your application's requirements without introducing unnecessary complexity.
12. Final Thoughts
Both Formik and React Hook Form solve the same fundamental problem: managing forms in React applications.
However, they approach that problem differently.
Formik provides a controlled, state-driven approach that many React developers find familiar.
React Hook Form primarily uses uncontrolled inputs, focuses on minimizing re-renders, and provides flexible APIs for managing complex forms.
If you're learning React, I would recommend implementing the same form using both libraries.
Don't just memorize their methods. Understand how values are stored, how validation works, what triggers re-renders, and how form submission is handled.
That's what will help you make better technical decisions when working on production applications.
What about you?
Which library do you prefer in your React projects — React Hook Form or Formik?
Have you faced any performance or maintenance challenges while working with large forms?
I'd love to hear about your experience in the comments.
Happy coding! 🚀
Top comments (0)