Skip to main content

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, useFormState for 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

PackageWeekly DownloadsTrend
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 FormConformTanStack Form
Downloads12M/week300K/week100K/week
Server ActionsAdded supportNativePlugin
Progressive enhancementPartialPartial
Type safetyGoodGoodExcellent
Bundle size~30KB~15KB~20KB
MaturityStableStableBeta
Zod integration@hookform/resolvers@conform-to/zodnative
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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.