Best React Form Libraries 2026: React Hook Form vs Conform vs TanStack Form
·PkgPulse Team
TL;DR
React Hook Form is still the default with 12M weekly downloads. Conform emerged as the go-to for Server Actions forms (uncontrolled, progressive enhancement, works without JS). TanStack Form is new but brings end-to-end type safety including field-level errors. For most apps: React Hook Form + Zod resolver. For Next.js with Server Actions: Conform. For type-safety fanatics who'll accept beta software: TanStack Form.
Key Takeaways
- React Hook Form: 12M downloads/week, Zod resolver,
useFormStatefor Server Actions, ~30KB - Conform: 300K downloads/week growing fast, built for Server Actions, progressive enhancement by default
- TanStack Form: ~100K downloads/week, alpha/beta, end-to-end type safety, framework-agnostic
- Formik: 7M downloads/week but declining — being replaced by React Hook Form
- Server Actions: Conform designed for it; RHF added support later; TanStack Form is framework layer
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
react-hook-form | ~12M | → Stable |
formik | ~7M | ↓ Declining |
@conform-to/react | ~300K | ↑ Growing fast |
@tanstack/react-form | ~100K | ↑ Growing |
React Hook Form: The Standard
// npm install react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const profileSchema = z.object({
name: z.string().min(1, 'Name required').max(100),
email: z.string().email('Invalid email'),
bio: z.string().max(500).optional(),
plan: z.enum(['free', 'pro', 'team']),
});
type ProfileForm = z.infer<typeof profileSchema>;
export function ProfileForm({ user }: { user: User }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<ProfileForm>({
resolver: zodResolver(profileSchema),
defaultValues: {
name: user.name,
email: user.email,
bio: user.bio ?? '',
plan: user.plan,
},
});
const onSubmit = async (data: ProfileForm) => {
await updateProfile(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('name')} placeholder="Display Name" />
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
</div>
<div>
<input {...register('email')} type="email" />
{errors.email && <p className="text-red-500">{errors.email.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</form>
);
}
React Hook Form + Server Actions
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { useForm } from 'react-hook-form';
import { updateProfile } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
export function ProfileFormWithServerAction() {
const [state, action] = useFormState(updateProfile, null);
const { register } = useForm({
defaultValues: { name: '' },
});
return (
<form action={action}>
<input {...register('name')} name="name" />
{state?.error?.name && <p>{state.error.name[0]}</p>}
<SubmitButton />
</form>
);
}
Conform: Built for Server Actions
// npm install @conform-to/react @conform-to/zod
'use client';
import { useForm, getFormProps, getInputProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState } from 'react-dom';
import { z } from 'zod';
import { updateProfile } from './actions';
const profileSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
bio: z.string().max(500).optional(),
});
export function ProfileForm() {
const [lastResult, action] = useFormState(updateProfile, undefined);
const [form, fields] = useForm({
lastResult,
onValidate: ({ formData }) => parseWithZod(formData, { schema: profileSchema }),
});
return (
<form {...getFormProps(form)} action={action}>
<div>
<input {...getInputProps(fields.name, { type: 'text' })} placeholder="Name" />
<p>{fields.name.errors}</p>
</div>
<div>
<input {...getInputProps(fields.email, { type: 'email' })} />
<p>{fields.email.errors}</p>
</div>
<button type="submit">Save</button>
</form>
);
}
// Server Action — Conform-compatible:
'use server';
import { parseWithZod } from '@conform-to/zod';
export async function updateProfile(prevState: unknown, formData: FormData) {
const submission = parseWithZod(formData, { schema: profileSchema });
if (submission.status !== 'success') {
return submission.reply(); // Returns errors to Conform client
}
await db.user.update({ where: { id: userId }, data: submission.value });
return submission.reply({ resetForm: true });
}
TanStack Form: Type-Safe (Beta)
// npm install @tanstack/react-form zod @tanstack/zod-form-adapter
import { useForm } from '@tanstack/react-form';
import { zodValidator } from '@tanstack/zod-form-adapter';
import { z } from 'zod';
export function ProfileForm() {
const form = useForm({
defaultValues: { name: '', email: '', bio: '' },
validators: {
onChange: z.object({
name: z.string().min(1),
email: z.string().email(),
bio: z.string().max(500).optional(),
}),
},
validatorAdapter: zodValidator(),
onSubmit: async ({ value }) => {
// value is fully typed
await updateProfile(value);
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field
name="name"
children={(field) => (
<div>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<p>{field.state.meta.errors.join(', ')}</p>
)}
</div>
)}
/>
<button type="submit">Save</button>
</form>
);
}
Comparison
| React Hook Form | Conform | TanStack Form | |
|---|---|---|---|
| Downloads | 12M/week | 300K/week | 100K/week |
| Server Actions | Added support | Native | Plugin |
| Progressive enhancement | Partial | ✅ | Partial |
| Type safety | Good | Good | Excellent |
| Bundle size | ~30KB | ~15KB | ~20KB |
| Maturity | Stable | Stable | Beta |
| Zod integration | @hookform/resolvers | @conform-to/zod | native |
Choose React Hook Form if:
→ Standard React app (vast majority of cases)
→ Team is familiar with it
→ Need stable, well-documented solution
Choose Conform if:
→ Next.js 15 with Server Actions
→ Progressive enhancement required
→ Server-side validation without JS
Choose TanStack Form if:
→ Maximum type safety is priority
→ Fine with beta software
→ Building form-heavy app where type inference matters
Compare react-hook-form, conform, and TanStack Form on PkgPulse.