react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-09-24 · react + react-native

Forms & validation

reviewedconfidence: mediumthis tier holds 71% on the public scorecard (7/20 graded · 0 overturned) →

related decisions: keyboard · meta-frameworks · state

cited by: state · keyboard

re-verified 6× — 2026-09-24 · 2026-09-10 · 2026-07-28 · 2026-07-13 · 2026-07-10 · 2026-07-06 · changelog

recommendation

React Hook Form + Zod (React and React Native). Uncontrolled fields keep typing off the render path, and one Zod schema validates both the form and the server mutation. The pick changes for multi-step, type-critical forms and for progressively enhanced server-action web forms (see when).

  • maximal type-safety / multi-step wizard → TanStack Form
  • server-action / progressive-enhancement web forms → Conform (web only)
  • same form logic across several UI frameworks → Formisch (1.x; needs no app-level setup, so migrate one form at a time)
  • bundle-sensitive validation → Valibot instead of Zod (Standard Schema keeps it swappable)
  • formatted/masked inputs (phone, card, currency) → rifm (a hook that wraps the input and composes with React Hook Form or TanStack Form; masking is a separate problem no form library owns)
  • existing Formik that works → keep Formik (migrate only when re-render cost is measured on long forms or weak devices)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
React Hook FormUncontrolled and subscription-based: keystrokes stay out of React's render loop unless something watches them. Runs on DOM and React Native. See note: STATE MODEL70.3M/wk · ships in 12/34
FormikControlled: every keystroke is React state, so long forms re-render the tree. Popular incumbent; not a new-project pick, kept where it already works.5.6M/wk · ships in 2/34
TanStack Form (1.x)Store-backed fields with narrow subscriptions and end-to-end type inference; FormGroup API for multi-step forms.4.2M/wk · ships in 1/34
Conform / Formisch (1.x)Conform = progressive enhancement for server-action web forms. Formisch = signal-based, schema-first (Valibot), stable since 1.0 (2026-08-20; 1.1.0 2026-09-08, verified vs npm 2026-09-24) across eight frameworks. See note: FORMISCH
Zod (4.6) / Valibot (validation)Schema validation consumed by any form lib; Standard Schema keeps them swappable. Zod 4.5 z.compile() parses objects/arrays/unions ~3–9x faster with ~9x less memory, so Valibot's remaining edge is bundle size, not speed. Zod 4.6.5 (2026-09-13, verified vs npm). See note: ZOD SPEED387.4M/wk · ships in 15/34

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): react-hook-form 70.3M · formik 5.6M · @tanstack/react-form 4.2M · zod 387.4M · valibot 25.5M

Verified notes

Forms are two decisions. First, where field state lives and what re-renders when it changes: uncontrolled (React Hook Form) keeps keystrokes out of React entirely, store-backed with narrow subscriptions (TanStack Form) keeps them in React but local, controlled (Formik, or hand-rolled useState) re-renders the form tree per keystroke. Second, where validation truth lives: in one schema (Zod or Valibot) imported by both the form and the server mutation, so the two cannot drift. Standard Schema makes the validator swappable, so neither decision locks you in. STATE MODEL (see reading 'React Hook Form Avoids State. TanStack Form Scopes It.', which owns the mechanism): both libraries stay fast; a working RHF form is rarely worth rewriting, and broad watch() calls are the path that re-couples RHF to re-renders. ZOD SPEED (2026-09-24, verified vs zod.dev blog 4.5 and 4.6): Zod 4.5 (2026-08-28) z.compile(schema) speeds parsing ~3–9x on objects, arrays and unions (complex schemas gain most); importing "zod/compile" at the entry point (or node --import zod/compile) compiles every schema defined after it; memory footprint dropped ~9x. Zod 4.6 (2026-09-09) adds .validate(), a boolean check that short-circuits on the first issue — up to 35x faster than .safeParse().success on invalid input with a compiled schema — and fixes a 4.5 out-of-memory regression in recursive schemas, so compiled or recursive-schema users should be on ≥4.6. Consequence: the runtime-speed argument for Valibot mostly no longer holds; the bundle-size argument does. FORMISCH (2026-08-20, verified vs formisch.dev v1 post): stable at 1.0 with a semver commitment (breaking changes only in majors, with upgrade notes), eight frameworks including Angular, six of them with migration guides. The Valibot schema and form logic survive a UI or framework change. MASKING (2026-08-29, verified vs npm and github.com/TrySound/rifm): input formatting (phone, card, currency) is not owned by any form library here. rifm (1.1.0, 2026-08-29; repo since 2018) is a small hook you wrap an input with, so it composes with RHF or TanStack Form rather than replacing either. RB-E-KEYBOARD owns RN form-screen keyboard behavior (insets, scrolling, focus travel); RB-E-META-FRAMEWORKS owns server actions as the submit path; RB-E-STATE owns why form state stays out of the global store. HISTORY: 2026-09-24 CORRECTED: the boolean fast path is Zod 4.6 .validate() (up to 35x on invalid input with z.compile), not a 4.5 z.validate "up to 16x" (no source found for 16x). 2026-09-10 GAP CLOSED: masked-input facet opened (rifm), correcting the react-status-488 skip; Zod 4.5 z.compile and Formisch 1.0 added.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

React Forms Done RightVu Nguyen (Upskills)

The decision arc of this entry in interactive form: a four-jobs mental model (capture, verify, recover, commit), then three archetypes — a React 19 useActionState + server-action + Zod form with no client form lib, a TanStack Form multi-step wizard (schema composition, AbortController async checks), and a field-array editable table with row-level re-render isolation — closing with when native React 19 suffices vs when a client library earns its keep. Covers the native-actions end of the spectrum the other reading here doesn't.

React Hook Form Avoids State. TanStack Form Scopes It.Adarsha Acharya

The mechanism behind this entry's RHF-vs-TanStack-Form choice, which the option rows only assert. RHF starts from REGISTERED inputs — the browser holds the value, so ordinary keystrokes never enter React's render loop — and adds React subscriptions only where other UI needs form state. TanStack Form starts from fields backed by a store: inputs are controlled, but only the field and its subscribers update. Both stay fast; they differ in where the default lands, and the post walks the specific code paths where that shows (field binding, watch/subscribe, arrays, async validation). Its own conclusion is the conservative one — a working RHF form is rarely worth rewriting. Examples checked against react-hook-form 7.82.0 and @tanstack/react-form 1.33.1.

One core, six frameworks, zero runtime abstractionFabian Hiller (Formisch / Valibot)

Shows build-time framework abstraction (a tiny core swapped by a bundler plugin) instead of a runtime adapter layer, so form state uses each framework's native signals. A generalizable library-design pattern reframing portability vs integration.

Sources

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About forms & validation — where state lives, where truth lives

Diataxis: Explanation. This page builds understanding of the forms landscape — the reasoning behind the picks. It is not a how-to. Keyboard behavior on RN form screens is owned by RB-E-KEYBOARD; server-side mutation machinery by RB-E-META-FRAMEWORKS.

The two questions that organise everything

A form library answers two independent questions, and conflating them is where teams go wrong:

  1. Where does field state live, and what re-renders when it changes? A controlled form (Formik's classic model) holds every keystroke in React state — simple mental model, but each keystroke re-renders the form tree. An uncontrolled form (React Hook Form's model) leaves state in the inputs and subscribes narrowly — the form only re-renders what watches. On a 30-field screen, or on a mid-range Android phone (RB-E-KEYBOARD territory), this is the difference between typing that feels native and typing that stutters.
  2. Where does validation truth live? The modern answer is: in a schema, once — Zod or Valibot — consumed by the form library through a resolver, and reused on the server for the same mutation. The form library is plumbing; the schema is the contract.

Standard Schema is the quiet piece that makes question 2 low-risk: a shared spec that lets form libraries accept any conforming validator, so Zod ↔ Valibot is a swap, not a rewrite.

The default, and why

React Hook Form + Zod — the common, performant, RN-compatible default.

RHF wins question 1 by default (uncontrolled, subscription-based — performance you don't have to earn) and runs on both DOM and React Native. Zod wins question 2 by ubiquity: it's the schema layer the rest of the 2026 stack already speaks (tRPC, server validation, RB-E-DATA inputs), so the form contract and the API contract can literally be the same object. Neither choice locks you in: Standard Schema keeps the validator swappable, and RHF's resolver keeps the schema library-agnostic.

The landscape, and when each earns its place

React Hook Form — the uncontrolled default; v7.8x keeps shipping (<FieldArray>, performance). Its API is register-and-subscribe; its cost model is "you pay for what you watch."

TanStack Form (1.x) — the type-safety maximalist: field values, errors, and touched state all inferred end-to-end, plus a FormGroup API for multi-step/wizard forms. Choose it when the form is the product (complex enrollment, builders) and type drift between steps is the real risk (RB-E-TYPESCRIPT mindset applied to form state).

Formik — the controlled incumbent. Don't migrate a working Formik app on principle; migrate when re-render cost actually bites (long forms, weak devices).

Conform — the progressive-enhancement specialist for the server-action web: forms that work before hydration and validate against the same schema on submit (RB-E-META-FRAMEWORKS).

Formisch — signal-based and schema-first (Valibot), stable since 1.0 (2026-08-20, semver committed) across eight frameworks; it needs no app-level setup, so one form can move over at a time. Its build-time framework abstraction (see reading) is as interesting as the library.

Zod / Valibot — the truth layer. Zod 4.5's z.compile() (~3–9x faster parsing, ~9x less memory) and Zod 4.6's short-circuiting .validate() removed most of the runtime-speed gap, so Valibot's remaining pitch is bundle size (tree-shakeable, matters at the marketing-page edge); Standard Schema makes the choice reversible.

Input masking — formatting phone, card, or currency input is a separate problem that none of the form libraries own. rifm is a small hook you wrap an input with, so it composes with RHF or TanStack Form instead of replacing them.

Tradeoffs and failure modes to name out loud

  • Validating in two places. UI-only validation drifts from server rules until a bad payload lands. One schema, imported by both sides, is the entire point of schema-first.
  • Controlled-by-default habits. Wiring every input to useState rebuilds Formik's cost model by hand — without the library. If state must be shared live, subscribe narrowly (RB-E-STATE's ownership-vs-subscription principle applies to forms too).
  • watch() and the Compiler. Broad watch() subscriptions are the escape hatch that re-couples RHF to re-renders — and the part that has surprised React Compiler adopters. Watch the narrowest slice that answers the question.
  • Wizard state in component state. Multi-step forms that stash progress in local state lose it on unmount/navigation; use FormGroup-style structures or lift to the router/URL.
  • RN forms that ignore the keyboard. The library manages values; it does not manage insets, scrolling, or focus travel — that's RB-E-KEYBOARD, and it's where RN form UX actually fails.

How it interacts with the rest of the stack

  • State (RB-E-STATE). Form state is short-lived UI state — it belongs in the form library, not the global store; only the submitted result graduates to server state (RB-E-DATA).
  • Meta-frameworks (RB-E-META-FRAMEWORKS). Server actions/functions are the submit endpoint; the schema travels there. Conform is this integration made first-class.
  • Keyboard (RB-E-KEYBOARD). On RN, form quality is mostly keyboard quality.
  • TypeScript (RB-E-TYPESCRIPT). Schemas are types-at-runtime; z.infer is the bridge that keeps form types and validation from diverging.

In one paragraph

Forms are two decisions dressed as one: where field state lives (uncontrolled wins by default — that's React Hook Form) and where validation truth lives (a schema, once — that's Zod, swappable via Standard Schema, with Valibot when bundle size matters). Reach for TanStack Form when end-to-end type inference and multi-step structure are the real requirements, Conform for progressively-enhanced server-action forms, and keep a working Formik until re-render cost is a measured problem. Then remember the part no form library does: on React Native, the form is only as good as its keyboard handling.


See also: RB-E-KEYBOARD (the RN half of form UX), RB-E-STATE (what is and isn't form state), RB-E-META-FRAMEWORKS (server actions as the submit path), RB-E-TYPESCRIPT (schema-inferred types).

Related in app-architecture: state · data · p2p · nav · meta-frameworks · auth · networking · uploads · crossplatform · desktop