DEV Community

Cover image for Formik vs React Hook Form: Which One Should You Choose for Your React Projects?
Sajal Sahu
Sajal Sahu

Posted on AI-assisted

Formik vs React Hook Form: Which One Should You Choose for Your React Projects?

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);
  },
});
Enter fullscreen mode Exit fullscreen mode

Formik provides useful handlers such as:

  • handleChange
  • handleBlur
  • handleSubmit
  • setFieldValue
  • setFieldTouched
  • setFieldError

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);
};
Enter fullscreen mode Exit fullscreen mode

Some important methods include:

  • register
  • handleSubmit
  • watch
  • setValue
  • reset
  • setError
  • useFieldArray
  • Controller

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
  • Email
  • Password
  • Confirm Password

Our requirements:

  1. All fields are required.
  2. Email must have a valid format.
  3. Password must contain at least 8 characters.
  4. Confirm Password must match Password.
  5. Display validation errors.
  6. Submit the form data.

Approach 1: Implementing the Form Using Formik

Step 1: Install the package

npm install formik
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)}
/>
Enter fullscreen mode Exit fullscreen mode

Here:

  1. React state stores the input value.
  2. The user types something.
  3. The onChange event updates the state.
  4. 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} />
Enter fullscreen mode Exit fullscreen mode

We can access the input value using:

console.log(inputRef.current.value);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"),
});
Enter fullscreen mode Exit fullscreen mode

Now pass this schema to Formik:

const formik = useFormik({
  initialValues: {
    fullName: "",
    email: "",
    password: "",
    confirmPassword: "",
  },

  validationSchema,

  onSubmit: (values) => {
    console.log(values);
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"),
});
Enter fullscreen mode Exit fullscreen mode

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),
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

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.",
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

You can display the server error like this:

{errors.email && (
  <p>{errors.email.message}</p>
)}

{errors.root?.server && (
  <p>{errors.root.server.message}</p>
)}
Enter fullscreen mode Exit fullscreen mode

And disable the submit button while the request is running:

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? "Submitting..." : "Register"}
</button>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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! 🚀


Useful Documentation

Top comments (0)