Skip to main content

Best React Form Libraries 2026: React Hook Form vs Conform vs TanStack Form

·PkgPulse Team

React Hook Form has 12 million weekly npm downloads. Its dominance is so complete that comparing it to alternatives feels unfair — but TanStack Form has first-class TypeScript inference that React Hook Form can't match, and Conform handles progressive enhancement with server actions better than either. The right choice depends on what "form handling" means in your application's architecture.

TL;DR

React Hook Form remains the default choice for most React applications in 2026 — battle-tested, performant, great ecosystem, and works well with React 19 server actions. TanStack Form is the right choice if you have deeply nested dynamic forms with complex TypeScript requirements. Conform is the right choice when progressive enhancement and server actions are architectural requirements (Next.js App Router forms that work without JavaScript).

Key Takeaways

  • React Hook Form: ~12M weekly downloads, ~41K GitHub stars
  • Conform (@conform-to/react): ~200K weekly downloads — fast growing with server actions adoption
  • TanStack Form (@tanstack/react-form): ~400K weekly downloads — growing
  • RHF uses uncontrolled inputs (faster); TanStack uses controlled inputs with fine-grained subscription
  • Conform is the only library with first-class progressive enhancement (works without JS)
  • All three integrate with Zod for validation
  • TanStack Form has the best TypeScript inference for nested/dynamic forms

React Hook Form

Package: react-hook-form Weekly downloads: ~12M GitHub stars: 41K Bundle: ~9 kB gzipped

The dominant form library in the React ecosystem. Its core innovation: using uncontrolled inputs (native browser inputs, ref-based) instead of controlled inputs means components only re-render on submit and on blur by default, not on every keystroke.

Basic Usage

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, 'Password must be at least 8 characters'),
  name: z.string().min(2, 'Name too short'),
});

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 createUser(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Name" />
      {errors.name && <span>{errors.name.message}</span>}

      <input {...register('email')} placeholder="Email" />
      {errors.email && <span>{errors.email.message}</span>}

      <input {...register('password')} type="password" placeholder="Password" />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating...' : 'Create Account'}
      </button>
    </form>
  );
}

With Server Actions (React 19)

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createPost } from '@/actions/post'; // Server action

function CreatePostForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(postSchema),
  });

  // Bridge RHF validation with server action
  async function onSubmit(data: PostFormData) {
    const result = await createPost(data);
    if (result?.error) {
      // Handle server errors
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* ... */}
    </form>
  );
}

Dynamic Fields

import { useFieldArray } from 'react-hook-form';

function TagsForm() {
  const { control, register } = useForm<{ tags: { value: string }[] }>();
  const { fields, append, remove } = useFieldArray({ control, name: 'tags' });

  return (
    <>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`tags.${index}.value`)} />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ value: '' })}>Add Tag</button>
    </>
  );
}

Ecosystem

@hookform/resolvers connects RHF to any validation library:

npm install @hookform/resolvers
# Then use with Zod, Yup, Valibot, ArkType, or Superstruct

Conform

Package: @conform-to/react, @conform-to/zod Weekly downloads: ~200K (growing rapidly with server actions adoption) GitHub stars: 4.5K

Conform is purpose-built for progressive enhancement with React server actions. Forms built with Conform work without JavaScript enabled — they degrade gracefully to native HTML form behavior.

Why Progressive Enhancement Matters

In Next.js App Router, server actions can be called from a <form action={serverAction}>. Without progressive enhancement, JavaScript must load before the form works. With Conform, the form submits via native HTTP POST if JavaScript hasn't loaded yet.

Basic Usage

// Server Action
'use server';
import { parseWithZod } from '@conform-to/zod';
import { redirect } from 'next/navigation';

const schema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

export async function signupAction(prevState: unknown, formData: FormData) {
  const submission = parseWithZod(formData, { schema });

  if (submission.status !== 'success') {
    return submission.reply(); // Returns structured errors to client
  }

  await createUser(submission.value);
  redirect('/dashboard');
}
// Client Component
'use client';
import { useForm, getInputProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState } from 'react-dom';
import { signupAction } from '@/actions/signup';

function SignupForm() {
  const [lastResult, action] = useFormState(signupAction, null);

  const [form, fields] = useForm({
    lastResult,
    onValidate({ formData }) {
      // Client-side validation with the same Zod schema
      return parseWithZod(formData, { schema });
    },
    shouldValidate: 'onBlur',
    shouldRevalidate: 'onInput',
  });

  return (
    // `action` is the server action — works even without JS
    <form id={form.id} onSubmit={form.onSubmit} action={action} noValidate>
      <input
        {...getInputProps(fields.name, { type: 'text' })}
        placeholder="Name"
      />
      {fields.name.errors && <p id={fields.name.errorId}>{fields.name.errors[0]}</p>}

      <input
        {...getInputProps(fields.email, { type: 'email' })}
        placeholder="Email"
      />
      {fields.email.errors && <p id={fields.email.errorId}>{fields.email.errors[0]}</p>}

      <button type="submit">Sign Up</button>
    </form>
  );
}

The form works as a standard HTML form (with server-side validation only) without JavaScript, and adds client-side validation enhancement when JavaScript loads.

Nested Forms and Field Lists

Conform excels at complex form shapes:

const [form, fields] = useForm({ schema: orderSchema });
const lines = fields.orderLines.getFieldList();

return (
  <form {...getFormProps(form)}>
    {lines.map((line, index) => {
      const lineFields = line.getFieldset();
      return (
        <div key={line.key}>
          <input {...getInputProps(lineFields.productId, { type: 'hidden' })} />
          <input {...getInputProps(lineFields.quantity, { type: 'number' })} />
          <input {...getInputProps(lineFields.price, { type: 'number' })} />
          <button {...form.remove.getButtonProps({ name: fields.orderLines.name, index })}>
            Remove
          </button>
        </div>
      );
    })}
    <button {...form.insert.getButtonProps({ name: fields.orderLines.name })}>
      Add Line
    </button>
  </form>
);

TanStack Form

Package: @tanstack/react-form Weekly downloads: ~400K GitHub stars: 5K Creator: Tanner Linsley (TanStack)

TanStack Form is the newest of the three, and its differentiator is first-class TypeScript inference for complex, dynamic forms.

Basic Usage

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

function SignupForm() {
  const form = useForm({
    defaultValues: {
      email: '',
      password: '',
      preferences: { newsletter: false, notifications: true },
    },
    validatorAdapter: zodValidator(),
    onSubmit: async ({ value }) => {
      // value is fully typed as:
      // { email: string, password: string, preferences: { newsletter: boolean, notifications: boolean } }
      await createUser(value);
    },
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
      <form.Field
        name="email"
        validators={{
          onChange: z.string().email('Invalid email'),
        }}
      >
        {(field) => (
          <>
            <input
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors.map(e => <span key={e}>{e}</span>)}
          </>
        )}
      </form.Field>

      <form.Subscribe selector={(state) => state.isSubmitting}>
        {(isSubmitting) => (
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>
        )}
      </form.Subscribe>
    </form>
  );
}

TypeScript Advantages

TanStack Form's biggest win: deep nested type inference:

const form = useForm({
  defaultValues: {
    user: {
      profile: {
        address: {
          street: '',
          city: '',
          zip: '',
        },
        preferences: { theme: 'light' as 'light' | 'dark' },
      },
    },
  },
});

// TypeScript KNOWS this is a 'light' | 'dark' string union
// React Hook Form doesn't provide this level of inference
<form.Field name="user.profile.preferences.theme">
  {(field) => <select value={field.state.value} ... />}
</form.Field>

Fine-Grained Subscriptions

TanStack Form's subscription model prevents unnecessary re-renders:

// Only re-renders when isSubmitting changes (not on every field change)
<form.Subscribe selector={(state) => state.isSubmitting}>
  {(isSubmitting) => <SubmitButton loading={isSubmitting} />}
</form.Subscribe>

// Only re-renders when the email field is dirty
<form.Subscribe selector={(state) => state.fieldMeta.email?.isDirty}>
  {(isDirty) => isDirty && <span>Unsaved changes</span>}
</form.Subscribe>

Feature Comparison

FeatureReact Hook FormConformTanStack Form
Weekly downloads12M200K400K
Bundle size~9 kB~8 kB~14 kB
TypeScript inferenceGoodGoodExcellent
Uncontrolled inputsYes (default)YesNo (controlled)
Progressive enhancementNoYesNo
Server actionsWorksNativeWorks
Dynamic arraysuseFieldArrayBuilt-inBuilt-in
Nested formsLimitedExcellentExcellent
Validation adapters10+Zod, YupZod, Valibot
Re-render optimizationGood (uncontrolled)GoodExcellent (subscriptions)

When to Choose Each

Choose React Hook Form if:

  • You want the most battle-tested solution with the largest community
  • Your forms are standard contact/signup/settings forms
  • You're using RHF already in the codebase
  • You want the widest choice of validation adapter libraries

Choose Conform if:

  • You're building forms in Next.js App Router with server actions
  • Progressive enhancement is required (government, accessibility requirements)
  • You prefer the formData-native server action pattern
  • Your forms are heavily nested with field arrays

Choose TanStack Form if:

  • You have deeply nested, dynamic forms with complex TypeScript types
  • You're already using TanStack ecosystem (Query, Router, Table)
  • Re-render optimization in complex forms matters
  • TypeScript autocompletion throughout the form is important

The Stack Recommendation for 2026

For most Next.js projects:

# Server actions + progressive enhancement
npm install @conform-to/react @conform-to/zod zod

# OR: Client forms + RHF
npm install react-hook-form @hookform/resolvers zod

Both patterns are production-proven. Choose based on whether your forms need progressive enhancement or follow a pure client-side model.

Compare these packages on PkgPulse.

Comments

Stay Updated

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