React Hook Form vs Formik 2026
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/resolverspackage 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
| Library | Minified | Gzipped |
|---|---|---|
| React Hook Form v7 | 28.4KB | 9.6KB |
| Formik v2 | 44.2KB | 15.5KB |
| TanStack Form v0 (core) | 18.1KB | 6.8KB |
| Final Form | 22.3KB | 8.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
React Hook Form + Zod (Recommended Stack 2026)
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:
registerno longer needs arefprop;Controllerrender 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
| Scenario | Recommendation |
|---|---|
| New React project | React Hook Form + Zod |
| Maintaining existing Formik codebase | Stay on Formik unless performance issues |
| Complex dynamic forms | React Hook Form (useFieldArray) |
| Large form (20+ fields) | React Hook Form (uncontrolled = no re-render cascade) |
| Team unfamiliar with uncontrolled components | Either 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.