<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/tanstack-form-vs-react-hook-form-vs-conform-react-forms-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/tanstack-form-vs-react-hook-form-vs-conform-react-forms-2026/raw.md -->
<!-- Source path: content/guides/tanstack-form-vs-react-hook-form-vs-conform-react-forms-2026.mdx -->

---
og_image: "/images/guides/tanstack-form-vs-react-hook-form-vs-conform-react-forms-2026.webp"
title: "TanStack Form vs react-hook-form vs Conform 2026"
description: "TanStack Form vs react-hook-form vs Conform for React forms in 2026: type safety, Server Actions, Zod validation, async checks, file uploads, and which to choose."
date: "2026-03-09"
authors: ["team"]
tier: 2
tags: ["react", "forms", "typescript", "nextjs"]
---

## TL;DR

**react-hook-form is still the safest default for most React forms.** It has the largest ecosystem, a small mental model around `register`, mature resolver support, and a performance profile that works well for everyday dashboards, settings screens, and multi-field product forms. **TanStack Form v1 is the strongest pick when type-safe field state, custom validation timing, async validation, and framework portability matter more than terse code.** **Conform is the best fit when your form is really a Next.js or Remix server workflow:** progressive enhancement, native `FormData`, and server-returned field errors are the core design rather than an integration afterthought.

The 2026 choice is less about raw speed and more about data flow. Pick react-hook-form for client-heavy React apps, TanStack Form for deeply typed interactive forms, and Conform for Server Actions or action/loader frameworks where the server should remain the source of truth.

## Key Takeaways

- **react-hook-form**: npm latest `7.76.0`; about 49.1M downloads in the 2026-05-09 to 2026-05-15 npm window; best default for broad React teams.
- **@tanstack/react-form**: npm latest `1.32.0`; about 1.9M downloads in the same npm window; best when typed field state, validation timing, and async validation are central requirements.
- **@conform-to/react**: npm latest `1.19.2`; about 153K downloads in the same npm window; best when HTML forms, Server Actions, Remix actions, and progressive enhancement are the architecture.
- **Server Actions are the dividing line.** Next.js documents Server Actions as server functions invoked from forms with `FormData`; Conform maps directly to that model, while react-hook-form and TanStack Form can integrate but usually add client-side orchestration.
- **Avoid benchmark absolutism.** All three can be fast enough. The practical differences are render subscriptions, validation timing, server round trips, and how much boilerplate your team accepts.

---

## 2026 Decision Matrix

| App requirement | Best pick | Why |
| --- | --- | --- |
| Most product forms, admin screens, settings pages | **react-hook-form** | Mature ecosystem, `register` API, resolver support, and low render churn with uncontrolled inputs. |
| Large typed forms with nested values and custom validation timing | **TanStack Form** | Field APIs expose typed state, synchronous and asynchronous validators, and explicit subscriptions. |
| Next.js App Router forms built around Server Actions | **Conform** | Uses native form submission, `FormData`, `parseWithZod`, and server-returned field errors. |
| Remix action forms | **Conform** | Its action-oriented model aligns with Remix's form/action workflow. |
| Framework-agnostic form model across React and other UI frameworks | **TanStack Form** | TanStack Form has a framework-agnostic core plus framework adapters. |
| Quick integration with shadcn/ui examples | **react-hook-form** | Most UI-kit docs and snippets still default to react-hook-form plus a schema resolver. |
| Heavy file-upload forms | **Conform** | Native `FormData` keeps files in the browser/server submission format instead of converting typed objects back into form data. |

---

## Source and Methodology Notes

This refresh checked the official docs and package metadata on 2026-05-16:

- TanStack Form official docs returned `200` at `https://tanstack.com/form/latest`, `https://tanstack.com/form/latest/docs/framework/react`, and the React validation docs. The automated preflight timeout was a network timing issue, not evidence that the docs disappeared.
- react-hook-form official docs returned `200` at `https://react-hook-form.com` and `https://react-hook-form.com/get-started`.
- Conform official docs returned `200` at `https://conform.guide` and `https://conform.guide/integration/nextjs` in browser validation.
- Next.js forms docs were checked for current Server Actions guidance: forms invoke server functions with the `action` attribute and receive `FormData`; validation errors can be surfaced with React `useActionState`.
- npm registry metadata and downloads were checked for the 2026-05-09 to 2026-05-15 window. Treat those download numbers as directional adoption signals, not permanent rankings.

---

## react-hook-form: Best Default for Client-Heavy React Forms

[react-hook-form](https://react-hook-form.com) is the pragmatic baseline because it solves the common React form problems with little ceremony: register inputs, validate with built-in rules or a schema resolver, subscribe to form state only where needed, and submit a typed object.

The key architectural choice is uncontrolled inputs. Instead of pushing every keystroke through React state, react-hook-form registers DOM inputs and updates React subscribers when validation or watched state changes. That is why it remains comfortable for large forms, even though you should still profile real production forms before making performance claims.

### Basic Zod Form

```typescript
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"

const alertSchema = z.object({
  packageName: z.string().min(1, "Choose a package"),
  threshold: z.coerce.number().min(1).max(100),
  email: z.string().email("Use a valid email"),
  alertType: z.enum(["downloads_drop", "version_update", "security"]),
})

type AlertFormData = z.infer<typeof alertSchema>

export function CreateAlertForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isDirty },
    setError,
    reset,
  } = useForm<AlertFormData>({
    resolver: zodResolver(alertSchema),
    defaultValues: {
      alertType: "downloads_drop",
      threshold: 10,
    },
    mode: "onSubmit",
    reValidateMode: "onChange",
  })

  const onSubmit = handleSubmit(async (values) => {
    const result = await createAlert(values)

    if (!result.ok) {
      setError("packageName", {
        type: "server",
        message: result.message ?? "Package not found",
      })
      return
    }

    reset()
  })

  return (
    <form onSubmit={onSubmit} noValidate>
      <label htmlFor="packageName">Package name</label>
      <input id="packageName" {...register("packageName")} />
      {errors.packageName ? <p role="alert">{errors.packageName.message}</p> : null}

      <label htmlFor="threshold">Drop threshold</label>
      <input id="threshold" type="number" {...register("threshold")} />
      {errors.threshold ? <p role="alert">{errors.threshold.message}</p> : null}

      <label htmlFor="email">Alert email</label>
      <input id="email" type="email" {...register("email")} />
      {errors.email ? <p role="alert">{errors.email.message}</p> : null}

      <select {...register("alertType")}>
        <option value="downloads_drop">Downloads drop</option>
        <option value="version_update">Version update</option>
        <option value="security">Security advisory</option>
      </select>

      <button type="submit" disabled={isSubmitting || !isDirty}>
        {isSubmitting ? "Creating…" : "Create alert"}
      </button>
    </form>
  )
}
```

### Where react-hook-form Wins

- **Ecosystem maturity:** examples, UI kit recipes, resolver packages, tutorials, and team familiarity are all strongest here.
- **Client-side form ergonomics:** `register`, `handleSubmit`, `Controller`, `watch`, `trigger`, and `setError` cover the common cases without forcing a custom form architecture.
- **Schema validation:** the resolver ecosystem lets you use Zod, Valibot, Yup, Ajv, Joi, ArkType, and other validators.
- **Performance-sensitive forms:** uncontrolled inputs avoid the naive "React state update per keypress" model, though validation mode and watched state can still cause renders.

### Where react-hook-form Needs Extra Care

- **Server Actions:** you can call a server action from a submit handler, but that is not the same as an HTML form whose `action` points directly to a server function. You need to decide how to map server errors back into `setError`, `reset`, or optimistic UI.
- **File uploads:** `register("file")` gives you a `FileList`, but object-style submit handlers often require rebuilding `FormData` before sending files to the server.
- **Conditional fields:** unregistered fields are not part of form state. That is usually desirable, but multi-step forms need deliberate `shouldUnregister`, hidden inputs, or server-side validation strategy.

---

## TanStack Form v1: Best for Typed Field State and Validation Control

[TanStack Form](https://tanstack.com/form) is now a v1 TanStack library rather than a speculative alpha. The official docs present it as a headless, framework-agnostic form library with first-class TypeScript support, field-level APIs, form-level APIs, and highly customizable validation.

The tradeoff is verbosity. TanStack Form asks you to model fields explicitly with `form.Field` and to decide when each validator runs. That is more code than `register`, but it gives you clearer control over nested values, async checks, form-level errors, and subscriptions.

### Field-Level Validation Example

```typescript
import { useForm } from "@tanstack/react-form"
import { z } from "zod"

const packageNameSchema = z
  .string()
  .min(1, "Choose a package")
  .regex(/^[a-z0-9._~@/-]+$/i, "Use a valid npm package name")

export function CreateAlertForm() {
  const form = useForm({
    defaultValues: {
      packageName: "",
      threshold: 10,
      email: "",
    },
    onSubmit: async ({ value }) => {
      await createAlert(value)
    },
  })

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        event.stopPropagation()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="packageName"
        validators={{
          onBlur: ({ value }) => {
            const result = packageNameSchema.safeParse(value)
            return result.success ? undefined : result.error.issues[0]?.message
          },
          onChangeAsyncDebounceMs: 500,
          onChangeAsync: async ({ value }) => {
            if (!value) return undefined
            const exists = await checkPackageExists(value)
            return exists ? undefined : "Package not found"
          },
        }}
      >
        {(field) => (
          <div>
            <label htmlFor={field.name}>Package name</label>
            <input
              id={field.name}
              name={field.name}
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(event) => field.handleChange(event.target.value)}
            />
            {field.state.meta.errors.map((error) => (
              <p key={String(error)} role="alert">{String(error)}</p>
            ))}
          </div>
        )}
      </form.Field>

      <form.Subscribe
        selector={(state) => [state.canSubmit, state.isSubmitting]}
      >
        {([canSubmit, isSubmitting]) => (
          <button type="submit" disabled={!canSubmit || isSubmitting}>
            {isSubmitting ? "Creating…" : "Create alert"}
          </button>
        )}
      </form.Subscribe>
    </form>
  )
}
```

### Where TanStack Form Wins

- **Validation timing:** the official docs emphasize that validation can happen on change, input, blur, submit, and asynchronously. That makes it a natural fit for expensive checks like package-name availability.
- **Typed field state:** field values, metadata, and error maps are exposed directly instead of hidden behind a registration spread.
- **Complex values:** arrays, nested objects, linked fields, and form composition are first-class documentation topics.
- **TanStack ecosystem fit:** teams already using TanStack Query, Router, Table, and Start often prefer a form library with similar explicit APIs.
- **Framework portability:** the form core and adapter model make it attractive for teams spanning React and other frontend frameworks.

### Where TanStack Form Needs Extra Care

- **Boilerplate:** simple login forms can look heavy compared with react-hook-form.
- **Server workflows:** TanStack Form has examples for framework/server integrations, but you still need to be deliberate about progressive enhancement if a plain HTML form should work before JavaScript loads.
- **Team familiarity:** fewer developers have production muscle memory with TanStack Form than with react-hook-form.

---

## Conform: Best for Server Actions and Progressive Enhancement

[Conform](https://conform.guide) is the most opinionated of the three: HTML forms and server-side validation are the center of the design. Its Next.js integration shows a schema, a server action using `parseWithZod`, and a client form that receives `lastResult` via React `useActionState`.

That model matters because React and Next.js forms increasingly run through server functions. Next.js documents that a form `action` can invoke a server function and receive a native `FormData` object. Conform embraces that shape instead of converting everything into a client-side object first.

### Next.js Server Action Example

```typescript
// app/create-alert/schema.ts
import { z } from "zod"

export const alertSchema = z.object({
  packageName: z.string().min(1, "Choose a package"),
  threshold: z.coerce.number().min(1).max(100),
  email: z.string().email("Use a valid email"),
})
```

```typescript
// app/create-alert/actions.ts
"use server"

import { parseWithZod } from "@conform-to/zod"
import { redirect } from "next/navigation"
import { alertSchema } from "./schema"

export async function createAlertAction(_prevState: unknown, formData: FormData) {
  const submission = parseWithZod(formData, { schema: alertSchema })

  if (submission.status !== "success") {
    return submission.reply()
  }

  await createAlert(submission.value)
  redirect("/alerts")
}
```

```typescript
// app/create-alert/page.tsx
"use client"

import { useActionState } from "react"
import { useForm, getFormProps, getInputProps } from "@conform-to/react"
import { parseWithZod } from "@conform-to/zod"
import { createAlertAction } from "./actions"
import { alertSchema } from "./schema"

export default function CreateAlertPage() {
  const [lastResult, action, isPending] = useActionState(createAlertAction, undefined)

  const [form, fields] = useForm({
    lastResult,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: alertSchema })
    },
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
  })

  return (
    <form {...getFormProps(form)} action={action}>
      <label htmlFor={fields.packageName.id}>Package name</label>
      <input {...getInputProps(fields.packageName, { type: "text" })} />
      <div id={fields.packageName.errorId}>{fields.packageName.errors}</div>

      <label htmlFor={fields.threshold.id}>Drop threshold</label>
      <input {...getInputProps(fields.threshold, { type: "number" })} />
      <div id={fields.threshold.errorId}>{fields.threshold.errors}</div>

      <label htmlFor={fields.email.id}>Alert email</label>
      <input {...getInputProps(fields.email, { type: "email" })} />
      <div id={fields.email.errorId}>{fields.email.errors}</div>

      <button type="submit" disabled={isPending}>Create alert</button>
    </form>
  )
}
```

### Where Conform Wins

- **Progressive enhancement:** a form can remain an HTML form with a real `action`, then enhance client-side validation when JavaScript is available.
- **Server truth:** validation happens against the same schema on the server action path that actually writes data.
- **File uploads:** native `FormData` is a better fit for files than object-based submit handlers.
- **Next.js and Remix ergonomics:** action results, field metadata, and error IDs are structured around the framework submission model.
- **Accessibility:** generated form and input props make it easier to keep labels, descriptions, and error regions associated.

### Where Conform Needs Extra Care

- **Client-only apps:** if your form never posts to a server action or action route, Conform's server-first model may feel like extra ceremony.
- **UI-kit wrappers:** native attributes work well, but some complex controlled components still need adapters.
- **Team familiarity:** fewer examples exist than for react-hook-form, and the mental model is different from typical client-state forms.

---

## Feature Comparison

| Feature | react-hook-form | TanStack Form v1 | Conform |
| --- | --- | --- | --- |
| Primary model | Uncontrolled registered inputs | Headless typed field/form APIs | Native HTML form + server result metadata |
| Best fit | Client-heavy React apps | Typed interactive forms | Server Actions and Remix actions |
| Latest npm version checked | `7.76.0` | `1.32.0` | `1.19.2` for `@conform-to/react` |
| Last-week npm downloads checked | ~49.1M | ~1.9M | ~153K for `@conform-to/react` |
| Zod support | `@hookform/resolvers/zod` | Standard Schema / validator callbacks | `@conform-to/zod` |
| Async validation | Supported, often manual per field | First-class validator timing including async validators | Usually server-action validation; client validation can revalidate |
| Server Actions | Possible, but you map action errors yourself | Possible, with framework examples and manual architecture choices | Core use case in Next.js integration |
| Progressive enhancement | Possible through native/browser features, not the default mental model | Requires deliberate architecture | Core design goal |
| File uploads | Works, but object-submit flows need care | Possible, but not the primary differentiator | Natural because `FormData` stays native |
| Learning curve | Lowest for most React teams | Medium to high | Medium if your team knows action-based forms |
| Ecosystem maturity | Highest | Growing quickly after v1 | Focused and framework-specific |

---

## How to Choose

### Choose react-hook-form if you want the safe default

Use react-hook-form when you are building ordinary React product forms: account settings, admin forms, package filters, dashboards, and modal workflows. It has the most examples, the broadest third-party component coverage, and enough escape hatches for server errors, async validation, and custom UI controls.

The default pattern is still react-hook-form plus Zod or Valibot resolver, with `Controller` only for custom controlled components.

### Choose TanStack Form if the form itself is complex state

Use TanStack Form when your form has enough logic that explicit field state is a benefit instead of a cost: nested arrays, dependent fields, debounced availability checks, wizard steps, form composition, typed error maps, or multiple validation timings on the same field.

It is not just a "newer react-hook-form." It is a different interface: more explicit, more typed, and often more verbose.

### Choose Conform if the server owns the form

Use Conform when the form should still submit correctly as HTML, the server action is the source of truth, and the browser should receive field errors from that server response. That is especially compelling for sign-up, checkout, contact, upload, and public submission flows in Next.js App Router or Remix.

If your application already treats every form as a client-side state object, Conform can feel unfamiliar. If your application treats forms as HTTP submissions with progressive enhancement, Conform feels natural.

---

## Practical Migration Notes

### Moving from react-hook-form to TanStack Form

Do this only when you need TanStack's explicit field state. The migration is not a mechanical rename:

1. Replace `register("field")` spreads with `form.Field name="field"` render props.
2. Move resolver-level validation into field-level or form-level validators.
3. Decide which validations run on change, blur, submit, or async debounce.
4. Rebuild UI-kit wrappers around `field.state.value`, `field.handleChange`, and `field.state.meta`.
5. Profile only after matching validation behavior; otherwise you are comparing different forms.

### Moving from react-hook-form to Conform

Do this when the form is becoming a Server Action workflow:

1. Move the schema next to the server action.
2. Parse the incoming `FormData` on the server with `parseWithZod` or another supported parser.
3. Return structured field errors instead of throwing for expected validation failures.
4. Wire the client form with `useActionState`, `lastResult`, and Conform's prop helpers.
5. Keep custom controlled widgets behind small adapters so the core form stays HTML-native.

### Moving from Conform to react-hook-form

Do this when the server-action architecture is unnecessary and the form is mostly client-side interaction. Keep the server validation as a final safety gate, but let react-hook-form manage local state and schema resolver feedback.

---

## Bottom Line

If you are searching for **TanStack Form vs React Hook Form in 2026**, the honest answer is not that one replaced the other. react-hook-form remains the default adoption and ergonomics winner. TanStack Form v1 is the strongest typed-state and validation-control option. Conform is the server-action-native option for teams leaning into Next.js and Remix form submissions.

For a broader non-comparison survey, see [Best React Form Libraries 2026](/guides/best-react-form-libraries-2026). For schema-library tradeoffs that affect all three choices, see [Zod vs Yup vs Valibot 2026](/guides/zod-vs-yup-vs-valibot-2026) and [Zod v4 vs ArkType vs TypeBox vs Valibot](/guides/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026).
