The Evolution of React Form Libraries: 2020–2026
TL;DR
React Hook Form won the form library wars — then native browser APIs and server actions started eating into its turf. React Hook Form (~3M weekly downloads) crushed Formik (~1.5M) on every metric that matters: bundle size, performance, TypeScript types. But in 2026, the most interesting form architectures skip form libraries entirely — using React 19 actions, useFormState, and native validation. Choose by what you're building, not by what was best in 2022.
Key Takeaways
- React Hook Form: ~3M weekly downloads — minimal re-renders, Zod integration, best DX
- Formik: ~1.5M downloads — legacy dominant, declining, high re-render count
- Conform: ~400K downloads — server action-compatible, the RHF of the server era
- @tanstack/react-form: ~200K — headless, type-safe, framework-agnostic
- React 19 actions — native
actionprop +useFormStatereduces need for libraries for simple forms
The 2020 Baseline: Formik Ruled
// 2020: Formik was the standard — but it had problems
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const SignupSchema = Yup.object({
email: Yup.string().email().required(),
password: Yup.string().min(8).required(),
});
function SignupForm() {
return (
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={SignupSchema}
onSubmit={(values, actions) => {
submitToAPI(values);
actions.setSubmitting(false);
}}
>
{({ isSubmitting, errors, touched }) => (
<Form>
<Field type="email" name="email" />
{errors.email && touched.email && <div>{errors.email}</div>}
<Field type="password" name="password" />
{errors.password && touched.password && <div>{errors.password}</div>}
<button type="submit" disabled={isSubmitting}>Submit</button>
</Form>
)}
</Formik>
);
}
// Problems:
// - Every keystroke triggers re-render (controlled inputs)
// - Complex state management via render props
// - Yup as only validation option (no Zod)
// - Large bundle (~13KB gzipped)
// - No TypeScript inference on field names
Formik's dominance from 2018-2021 was built on solving a genuine problem: React had no built-in form state management, and the emerging convention of controlled inputs required wiring up onChange handlers, maintaining state for every field, and managing validation and submission manually. Formik packaged that ceremony into a standard API that thousands of teams adopted.
The performance problem wasn't obvious until applications scaled. A login form with two fields didn't surface the re-render issue. A complex settings page with 15 fields and conditional validation — where checking one field's value should affect another's validation state — exposed the controlled input model's limits. Every character typed in any field triggered a re-render of every field. On older hardware or in performance-sensitive applications, this created visible jank.
The render props API ({({ isSubmitting, errors, touched }) => ...}) also had ergonomic costs that weren't apparent until teams tried to compose complex forms from multiple components. Accessing form state from a component three levels deep required either prop drilling all the way down or reaching for Formik's useFormikContext() hook, which tied child components to the Formik rendering model rather than being reusable across form contexts. Passing the form context through render props required either deep prop drilling or React Context — and the Context approach had its own performance implications. By 2021, teams building sophisticated form interactions regularly cited Formik's re-render behavior as the thing they'd change first if starting over.
React Hook Form's emergence wasn't a surprise — it solved a documented problem with a clearly superior architectural choice. The surprise was how quickly it displaced Formik in new projects once word spread. Blog posts comparing re-render counts with React DevTools visualizations made the difference concrete and shareable.
2022: React Hook Form Takes Over
// React Hook Form v7 — the defining API
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'),
password: z.string().min(8, 'At least 8 characters'),
});
type FormData = z.infer<typeof schema>;
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await submitToAPI(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>Submit</button>
</form>
);
}
// Why RHF won:
// - Uncontrolled inputs → no re-render on keystroke
// - Zod resolver → end-to-end type safety
// - register() returns all needed props in one spread
// - errors are typed: errors.email.message = string | undefined
// - ~9KB gzipped vs Formik's ~13KB
React Hook Form's adoption curve between 2020 and 2023 was shaped by a combination of technical superiority and excellent timing. The library launched in 2019, just as TypeScript was becoming the default for new React projects and Zod was emerging as the preferred runtime validation library. The zodResolver integration — introduced in @hookform/resolvers — created a compound advantage: type-safe schemas that generated both TypeScript types (z.infer<typeof schema>) and runtime validation in a single declaration.
The uncontrolled input model required a mental model shift for developers deeply familiar with Formik. The register() function returns the props needed for native input elements (ref, onChange, onBlur, name), spread directly onto the input. The form state — field values, errors, dirty state — lives in a ref, not in React state, which is why keystrokes don't trigger re-renders. Developers who understood this model immediately saw why it scaled; developers who tried to treat RHF like Formik (using watch() everywhere, for instance) recreated the re-render problem they were trying to escape.
The migration from Formik to React Hook Form is mechanical enough that automated tools exist for it. The main gotchas: Formik's setFieldValue equivalent in RHF is setValue, setFieldError becomes setError, and Formik's <Field> component's component prop has no direct equivalent — you use Controller from RHF for custom input components. Teams with good Formik test coverage typically discovered all their migration issues during the test run, which is the recommended approach.
2024: shadcn/ui Standardizes the Pattern
// The de facto 2024 pattern: RHF + Zod + shadcn/ui Form components
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Form, FormField, FormItem, FormLabel,
FormControl, FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type FormData = z.infer<typeof schema>;
function SignupForm() {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', confirmPassword: '' },
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage /> {/* Shows error automatically */}
</FormItem>
)}
/>
{/* Repeat for other fields */}
<Button type="submit" disabled={form.formState.isSubmitting}>
Create Account
</Button>
</form>
</Form>
);
}
shadcn/ui's Form component, released in late 2023, had an outsized impact on how React Hook Form is used in production codebases. The component ships with the shadcn/ui registry and provides FormField, FormItem, FormLabel, FormControl, and FormMessage — a complete abstraction layer over RHF's lower-level API. Teams that adopt shadcn/ui get an opinionated, accessible form pattern with built-in error display and label association without needing to design it themselves.
The effect on the ecosystem: the RHF + Zod + shadcn/ui stack became the default recommendation in Next.js communities, Discord servers, and blog posts to the point of near-ubiquity. Teams starting new Next.js projects in 2024 rarely made form library decisions from scratch — they adopted the stack because every resource they found assumed it. This kind of ecosystem standardization has compounding benefits: stack familiarity reduces onboarding friction for new developers, AI coding assistants generate accurate code for well-represented patterns, and shadcn/ui's open copy-paste model means teams can audit and modify the form components they're building on.
The pattern shadcn/ui codified also has a specific limitation worth understanding: it's designed for traditional client-side form validation and submission. The Form component doesn't have a native path to React 19 server actions — teams that want server-action-based forms with shadcn/ui components combine them with Conform as the form state layer, treating shadcn's visual components as presentation-only while Conform handles the action integration. This combination works well but requires understanding both libraries independently.
2026: Server Actions Enter the Picture
// React 19: native form actions — no form library needed for simple forms
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
// Server action — validates on server
async function createAccount(prevState: unknown, formData: FormData) {
const raw = Object.fromEntries(formData);
const parsed = schema.safeParse(raw);
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors };
}
await db.user.create({ data: parsed.data });
return { success: true };
}
// Client component using the server action
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createAccount } from '@/actions/auth';
function SignupForm() {
const [state, formAction] = useFormState(createAccount, null);
return (
<form action={formAction}>
<input name="email" type="email" />
{state?.errors?.email && <span>{state.errors.email[0]}</span>}
<input name="password" type="password" />
{state?.errors?.password && <span>{state.errors.password[0]}</span>}
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus(); // Must be inside <form>
return <button type="submit" disabled={pending}>Sign Up</button>;
}
// Conform — when you want RHF DX with server actions
// Best of both worlds: type-safe + server-compatible
import { useForm, getFormProps, getInputProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
function SignupForm() {
const [form, fields] = useForm({
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
// Works with server actions
onSubmit(event, { formData }) {
event.preventDefault();
createAccount(formData); // Can be a server action
},
});
return (
<form {...getFormProps(form)}>
<input {...getInputProps(fields.email, { type: 'email' })} />
<div>{fields.email.errors}</div>
<input {...getInputProps(fields.password, { type: 'password' })} />
<div>{fields.password.errors}</div>
<button type="submit">Sign Up</button>
</form>
);
}
Server actions change the form library calculus for Next.js App Router projects in a specific way: the form submission and server-side validation happen in the same network request, and the response — including validation errors — comes back as the action's return value rather than through a client-side API call. This is closer to traditional HTML form behavior than React's typical SPA form pattern.
The useFormState hook (renamed to useActionState in React 19) bridges server action responses back into the component tree. The ergonomics for simple forms are compelling: no client-side validation library, no submission handler, no loading state management — the browser handles form serialization, the server action handles validation, and useFormState connects the response to the UI. For a contact form or newsletter signup, this is the right architecture.
The complexity emerges at the boundaries. Progressive enhancement — where the form works without JavaScript and then gets enhanced when JavaScript loads — is a legitimate goal that server actions enable. But multi-step forms, conditional validation (field B validates against field A's value), and real-time field feedback (showing password strength as the user types) still require client-side state. Conform handles these by running the same Zod schema client-side for instant feedback and server-side for security validation, with the server action as the source of truth.
The 2026 pattern that works across complexity levels: simple forms (1-4 fields, no interdependencies) use native server actions without a library. Complex forms (5+ fields, conditional logic, multi-step, real-time validation) use React Hook Form (client-only context) or Conform (server action context). TanStack Form is worth evaluating for new projects that need maximum framework portability — its API is designed to work identically in React, Solid, and Vue contexts, which matters for monorepos with multiple frontends.
Form Library Evolution Timeline
| Year | Dominant Pattern | Bundle | Re-renders | Server-compatible |
|---|---|---|---|---|
| 2020 | Formik + Yup | ~13KB | Many | ❌ |
| 2022 | RHF + Zod | ~9KB | Minimal | ❌ (client-only) |
| 2024 | RHF + Zod + shadcn/ui Form | ~9KB | Minimal | ❌ |
| 2026 | RHF (client) / Conform (server) / Native (simple) | 0-9KB | Minimal | ✅ |
When to Choose
| Scenario | Pick |
|---|---|
| Complex client-side form | React Hook Form + Zod |
| shadcn/ui component library | RHF (built in) |
| Next.js app with server actions | Conform |
| Simple form (1-3 fields) | Native <form> + useFormState |
| Multi-step wizard | RHF with multiple schemas |
| Headless, no library lock-in | @tanstack/react-form |
| Migrating from Formik | React Hook Form (API migration guide in v7 docs) |
Multi-Step Forms and Advanced RHF Patterns
Multi-step forms remain the scenario where React Hook Form shows its flexibility most clearly. The recommended pattern: keep a single useForm instance at the top level and pass control and specific error objects down to each step's component. This preserves validation state across steps without losing typed data or re-running validation unnecessarily.
An alternative for large multi-step flows: use separate useForm instances per step, accumulate submitted step data in a parent's useState, and merge at the final submission. This approach is simpler to reason about when each step has genuinely independent validation requirements and you don't need cross-step validation at any point.
The useFormContext hook enables consumption of the parent form's context from deeply nested components without prop drilling — the shadcn/ui FormField uses this internally. For custom components that need to register with the parent form (a date picker, a custom select, a rich text editor), Controller provides the bridge between RHF's ref-based model and components that manage their own internal state.
Zod schemas compose well with multi-step forms through z.object() intersection: define each step's schema, then stepOneSchema.merge(stepTwoSchema) for the final server-side validation. This ensures that final submission validation covers all fields even if the client navigates directly to the final step — a security requirement if any multi-step form allows navigation between steps after partial completion.
The TypeScript story for multi-step forms is one of RHF's strongest advantages. FormData = z.infer<typeof schema> gives you a complete typed object for the form's data at every step. The errors object is typed to the same shape, meaning TypeScript prevents accessing errors for fields that don't exist in the schema. This compile-time correctness, particularly in multi-step forms where different schemas apply to different steps, saves hours of debugging that Formik's looser typing couldn't prevent.
Performance: Why Uncontrolled Inputs Changed Everything
The performance difference between Formik and React Hook Form is not marginal — it's architectural. Formik's controlled input model subscribed every <Field> to the form's state. On a 15-field form, every keystroke triggered 15 re-renders. Modern React DevTools can visualize this clearly: flash the component tree while typing in a Formik form and every field lights up simultaneously.
React Hook Form's uncontrolled model registers inputs with native DOM refs. The form state lives in a ref, not in React state. Keystrokes go directly to the DOM without triggering React re-renders. Only the specific field being validated re-renders when it validates.
This is not just a performance optimization — it changes what's possible. A form with dependent validation logic (field B validates against field A's value) can be implemented in RHF without unnecessary cascading re-renders. The watch() function is the escape hatch: useWatch({ name: 'email' }) subscribes a component to a specific field's changes. Using watch() everywhere reintroduces re-renders, which is why the documentation specifically advises against it for performance-critical forms.
The shadcn/ui Form component abstraction wraps RHF with a clean API that avoids common watch() overuse — it's worth studying as a reference implementation of RHF best practices.
From Yup to Zod to Valibot: The Validation Stack
The validation library story runs parallel to the form library story. Formik standardized on Yup (an OOP-style schema builder). React Hook Form introduced zodResolver via @hookform/resolvers, making Zod the new standard for form validation schemas. By 2024, zod was a near-default dependency for any TypeScript project with user input.
In 2026, Valibot is the next evolution: it achieves similar validation capability at a fraction of the bundle size (~2KB tree-shaken vs Zod's 14KB tree-shaken) by making each validator a separate import. The @hookform/resolvers/valibot integration exists for RHF, making migration straightforward.
The practical tradeoff: Zod's error messages and z.infer<> type inference have better ecosystem support. shadcn/ui components default to Zod. tRPC's input() schemas are typically Zod. If your project uses both RHF forms and tRPC API calls, Zod's shared schema advantage often outweighs Valibot's bundle size benefit.
For projects where bundle size is critical (e-commerce, landing pages), Valibot's per-rule pricing makes it the right choice.
The practical consideration that often settles the Zod vs Valibot question: does your backend also use the same schema library? In a full-stack TypeScript project where the frontend and backend share a monorepo, using the same validation library means sharing schema definitions between the API input validation and the form validation layer. If your backend already uses Zod (common in tRPC setups), form validation in Zod avoids teaching two different schema APIs to developers who work across the stack. Schema sharing reduces the single biggest source of form validation bugs: frontend and backend having different opinions about what constitutes a valid input. A shared Zod schema is the source of truth for both layers simultaneously, which makes that class of bug structurally impossible.
The Download Data Behind the Shift
The form library download trends tell a clear adoption story. Formik peaked at ~2M weekly downloads in 2021 and has declined steadily — currently ~1.5M in 2026, with most downloads from legacy projects maintaining existing code rather than new projects choosing it.
React Hook Form's trajectory was the inverse: launched in 2019, crossed Formik in downloads during 2022, now sits at ~3M weekly. The growth hasn't come from Formik refugees alone — it reflects the growth of React itself and the standardization on RHF as the default for new projects.
The newer entrants tell a different story. Conform's ~400K weekly downloads is concentrated in Next.js App Router projects — it's the obvious choice when you're using server actions, and App Router adoption is still growing. TanStack Form's ~200K downloads is notably strong for a library launched in 2024; it benefits from TanStack brand recognition and the pattern of "if you're already using TanStack Query, use TanStack Form for consistency."
The native browser API approach (no library) doesn't show up in npm downloads, but its presence is real — teams building simple forms increasingly choose useFormState + native validation over adding another dependency.
The 2026 picture: React Hook Form continues to dominate client-side forms, but the "share of forms handled by a library" is declining as RSC and server actions make library-free forms practical for simple cases.
The signal worth watching: AI coding assistants have standardized on the RHF + Zod + shadcn/ui pattern. When a developer asks Cursor, GitHub Copilot, or Claude to generate a form component, the output almost universally uses this stack. That standardization has a reinforcing effect — new developers learn this pattern first, use it by default, and don't evaluate alternatives unless they hit a specific limitation. For Conform, TanStack Form, and other alternatives to grow meaningfully, they'll need to either be included in AI training data for common patterns or offer enough differentiated value that developers actively seek them out. Conform is well-positioned for server action workflows; TanStack Form for multi-framework teams. Neither has achieved the ubiquity that triggers automatic AI generation.
Compare form library package health on PkgPulse.
See also: React vs Vue and React vs Svelte, Best React Form Libraries in 2026.
See the live comparison
View react hook form vs. formik on PkgPulse →