Skip to main content

React Hook Form vs Formik 2026

·PkgPulse Team
0

React Hook Form vs Formik 2026

TL;DR

React Hook Form has won the form library debate in 2026. With 10M+ weekly npm downloads vs Formik's 2.5M (and declining), RHF's uncontrolled component architecture that eliminates re-renders, and a 9.6KB gzipped bundle vs Formik's 15.5KB, the performance case is unambiguous. Formik hasn't had a major release since 2021 and is effectively in maintenance mode. Use React Hook Form with Zod for new projects. Formik is still fine if you're maintaining existing code — but don't migrate for the sake of it.

Key Takeaways

  • React Hook Form has ~10.4M weekly npm downloads; Formik has ~2.5M (declining since 2022)
  • RHF bundle size: 9.6KB gzipped — 38% smaller than Formik (15.5KB gzipped)
  • Performance: RHF uses uncontrolled components — form inputs don't re-render the parent on every keystroke; Formik re-renders the entire form on each change
  • Formik last major release: v2.4.6 (2023) — no active feature development
  • RHF latest: v7.x — actively maintained, React 19 compatible
  • Zod integration: RHF's @hookform/resolvers package supports Zod, Yup, Joi, Valibot, and 10+ others
  • Formik + Yup: The classic combo still works but Yup's TypeScript inference is weaker than Zod's
  • TanStack Form is the emerging challenger — same team as TanStack Query, framework-agnostic, but still v0 as of March 2026

Why Form Libraries Exist

Native HTML form handling in React has a fundamental tension: React wants to control all state (controlled components), but tracking each keystroke in state triggers a re-render on every change. For a form with 20 fields, that's 20 simultaneous state updates per keystroke.

The libraries solve this differently:

Formik: Embraces controlled components. Every input value lives in Formik's state. Clean, predictable, but expensive at scale.

React Hook Form: Uses uncontrolled components under the hood. Inputs are registered via ref, values are read directly from the DOM when needed. Only re-renders when validation state changes, not on every keystroke.


Bundle Size and Performance

Bundle Size

LibraryMinifiedGzipped
React Hook Form v728.4KB9.6KB
Formik v244.2KB15.5KB
TanStack Form v0 (core)18.1KB6.8KB
Final Form22.3KB8.1KB

RHF is 38% smaller gzipped — meaningful for initial page load, especially on mobile connections.

Re-render Performance

The more important difference is runtime performance. Formik re-renders the parent component on every input change. RHF only re-renders when:

  • The form is submitted
  • Validation errors change
  • You explicitly subscribe to a field's value with watch()

Formik re-render example:

// This re-renders the entire FormikForm on EVERY keystroke
function FormikForm() {
  return (
    <Formik initialValues={{ name: '', email: '' }} onSubmit={handleSubmit}>
      {({ values, errors }) => (
        <Form>
          <Field name="name" /> {/* triggers re-render on each key */}
          <Field name="email" />
          <button type="submit">Submit</button>
        </Form>
      )}
    </Formik>
  )
}

RHF with no re-renders:

// Only re-renders when errors change or form is submitted
function RHFForm() {
  const { register, handleSubmit, formState: { errors } } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name', { required: true })} />
      {errors.name && <span>Name is required</span>}
      <input {...register('email', { required: true, pattern: /\S+@\S+\.\S+/ })}/>
      <button type="submit">Submit</button>
    </form>
  )
}

For a 5-field form, the difference is negligible. For a 30-field dynamic form with complex validation, RHF's approach can reduce re-renders by 95%+.


Validation Integration

The @hookform/resolvers package bridges RHF with any schema validation library. Zod is the 2026 default because of superior TypeScript inference:

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain at least one uppercase letter'),
  age: z.number().int().min(18, 'Must be 18 or older').optional(),
})

type FormData = z.infer<typeof schema> // Full TypeScript inference

function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<FormData>({
    resolver: zodResolver(schema),
  })

  const onSubmit = async (data: FormData) => {
    // data is fully typed — TypeScript knows data.email is string, etc.
    await registerUser(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('email')} placeholder="Email" />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
      <div>
        <input {...register('password')} type="password" placeholder="Password" />
        {errors.password && <p>{errors.password.message}</p>}
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Registering...' : 'Register'}
      </button>
    </form>
  )
}

Key advantage: FormData type is inferred directly from the Zod schema. Change the schema → TypeScript errors guide you to fix the form. No type duplication.

Formik + Yup

import { Formik, Form, Field, ErrorMessage } from 'formik'
import * as Yup from 'yup'

const schema = Yup.object({
  email: Yup.string().email('Invalid email').required('Required'),
  password: Yup.string()
    .min(8, 'Must be at least 8 characters')
    .required('Required'),
})

// Formik requires manually typing the values — no inference from Yup schema
interface FormValues {
  email: string
  password: string
}

function RegisterForm() {
  return (
    <Formik
      initialValues={{ email: '', password: '' } as FormValues}
      validationSchema={schema}
      onSubmit={async (values) => {
        await registerUser(values)
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <Field name="email" type="email" placeholder="Email" />
          <ErrorMessage name="email" component="p" />
          <Field name="password" type="password" placeholder="Password" />
          <ErrorMessage name="password" component="p" />
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Registering...' : 'Register'}
          </button>
        </Form>
      )}
    </Formik>
  )
}

The Yup + Formik pattern requires maintaining the FormValues interface separately from the Yup schema — a type duplication that Zod + RHF eliminates.


Complex Scenarios: Nested Fields and Field Arrays

Dynamic Field Arrays (RHF)

RHF's useFieldArray hook is elegant for dynamic lists:

import { useForm, useFieldArray } from 'react-hook-form'

function TeamForm() {
  const { control, register, handleSubmit } = useForm({
    defaultValues: { members: [{ name: '', role: '' }] }
  })

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'members'
  })

  return (
    <form onSubmit={handleSubmit(console.log)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`members.${index}.name`)} placeholder="Name" />
          <input {...register(`members.${index}.role`)} placeholder="Role" />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: '', role: '' })}>
        Add Member
      </button>
      <button type="submit">Save Team</button>
    </form>
  )
}

Dynamic Field Arrays (Formik)

import { Formik, Form, Field, FieldArray } from 'formik'

function TeamForm() {
  return (
    <Formik
      initialValues={{ members: [{ name: '', role: '' }] }}
      onSubmit={console.log}
    >
      {({ values }) => (
        <Form>
          <FieldArray name="members">
            {({ push, remove }) => (
              <>
                {values.members.map((_, index) => (
                  <div key={index}>
                    <Field name={`members.${index}.name`} placeholder="Name" />
                    <Field name={`members.${index}.role`} placeholder="Role" />
                    <button type="button" onClick={() => remove(index)}>Remove</button>
                  </div>
                ))}
                <button type="button" onClick={() => push({ name: '', role: '' })}>
                  Add Member
                </button>
              </>
            )}
          </FieldArray>
          <button type="submit">Save Team</button>
        </Form>
      )}
    </Formik>
  )
}

Both handle field arrays. RHF's useFieldArray generates stable field.id keys automatically (critical for React reconciliation). Formik uses array index as key — a common source of bugs when items are removed.


Ecosystem State in 2026

React Hook Form

  • Downloads: ~10.4M/week and growing
  • GitHub stars: 40K+
  • Last major release: v7 (2023), actively maintained
  • React 19 compatible:
  • Resolvers: 15+ official resolvers (Zod, Yup, Joi, Valibot, ArkType, TypeBox...)
  • DevTools: @hookform/devtools — visualizes form state during development
  • RHF v7 breaking changes from v6: register no longer needs a ref prop; Controller render prop signature changed

Formik

  • Downloads: ~2.5M/week and declining (down from ~3.5M peak in 2021)
  • GitHub stars: 34K (mostly from earlier popularity)
  • Last release: v2.4.6 (2023 — bug fixes only)
  • React 19 compatible: Yes (no known breaking issues, but untested officially)
  • Active development: Minimal — maintainers have stated they're primarily in maintenance mode
  • Formik v3: Was planned; hasn't materialized. The maintainer Jared Palmer has focused on TurboRepo/Vercel instead.

TanStack Form (Emerging)

TanStack Form is the framework-agnostic successor being built by the TanStack team. It shares the same monorepo as TanStack Query/Router:

import { useForm } from '@tanstack/react-form'
import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'

const form = useForm({
  defaultValues: { email: '', password: '' },
  validatorAdapter: zodValidator(),
  validators: {
    onSubmit: z.object({
      email: z.string().email(),
      password: z.string().min(8),
    })
  },
  onSubmit: async ({ value }) => console.log(value),
})

TanStack Form's reactive store model means it works equally well in React, Vue, Solid, and Angular. If you're already invested in TanStack Query + TanStack Router, TanStack Form is the natural fit — but it's v0 as of March 2026 and not production-ready for most teams.


Migration: Formik → React Hook Form

If you have an existing Formik form worth migrating:

// BEFORE: Formik
<Formik
  initialValues={{ email: '', name: '' }}
  validate={values => {
    const errors: any = {}
    if (!values.email) errors.email = 'Required'
    return errors
  }}
  onSubmit={handleSubmit}
>
  {({ errors, isSubmitting }) => (
    <Form>
      <Field name="email" />
      {errors.email && <div>{errors.email}</div>}
      <button type="submit" disabled={isSubmitting}>Submit</button>
    </Form>
  )}
</Formik>

// AFTER: React Hook Form + Zod
const schema = z.object({
  email: z.string().min(1, 'Required').email('Invalid email'),
  name: z.string(),
})

const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
  resolver: zodResolver(schema)
})

// <form> replaces <Form>, native <input> replaces <Field>
<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register('email')} />
  {errors.email && <div>{errors.email.message}</div>}
  <button type="submit" disabled={isSubmitting}>Submit</button>
</form>

The migration is mostly mechanical — replace <Formik>, <Form>, <Field> components with useForm(), native <form>, and <input> with {...register('fieldName')}. The main adjustment is the error access pattern: errors.email (string) → errors.email?.message (string | undefined).


Recommendations

ScenarioRecommendation
New React projectReact Hook Form + Zod
Maintaining existing Formik codebaseStay on Formik unless performance issues
Complex dynamic formsReact Hook Form (useFieldArray)
Large form (20+ fields)React Hook Form (uncontrolled = no re-render cascade)
Team unfamiliar with uncontrolled componentsEither works; Formik's controlled model may be easier to reason about
Full TanStack stack (Query + Router)Watch TanStack Form — not yet v1

Methodology

  • Sources: npm trends (react-hook-form, formik, @tanstack/form-core — March 2026), bundlephobia.com bundle size data (March 2026), React Hook Form GitHub releases, Formik GitHub (last commit dates, issue activity), TanStack Form GitHub (v0 status), Formik maintainer statements (GitHub discussions), Reddit r/reactjs comparison threads (2024-2025), State of JavaScript 2025 survey (form library usage), official documentation for RHF v7 and Formik v2
  • Data as of: March 2026

Need to handle file uploads in your forms? See Best File Upload Libraries React 2026.

Validating on the backend too? See Zod vs Yup vs Valibot 2026 for schema validation library comparisons.

Compare Formik and React Hook Form package health on PkgPulse.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.