Skip to main content

Tanstack Forms

Forms are where users meet friction. Every field is a chance to lose them.

The right form library removes boilerplate without hiding control. The wrong one fights your validation logic at scale.

Comparison

CriteriaTanStack FormReact Hook FormNative HTML
Bundle size~15 KB~9 KB0 KB
ValidationBuilt-in + Zod/ValibotResolver pattern (Zod)Manual
Render controlFine-grained subscriptionUncontrolled by defaultFull control
Server ComponentsCompatibleCompatibleNative
TypeScriptFirst-classFirst-classManual types
Learning curveModerateLowNone
Multi-step formsBuilt-inManual orchestrationManual
Best forComplex forms, wizardsSimple-to-medium formsOne-off forms

Key Patterns

PatternWhenWhy
Schema validationAlwaysSingle source of truth for client and server
Progressive disclosureMulti-step flowsReduce cognitive load per screen
Optimistic submissionLow-risk actionsFaster perceived performance
Server-side validationAlwaysClient validation is a courtesy, not security
Field-level errorsUser-facing formsImmediate feedback beats form-level error dumps

Progressive Enhancement

The action attribute on <form> accepts a server action directly. The form submits to the server whether JavaScript has loaded or not. A client function (() => form.handleSubmit()) requires hydration first — the form is dead until then.

TierPatternWorks Before JSBundle Cost
1. Native<form action={serverAction}> + name attrsYes0 KB
2. EnhancedTanStack validates client-side, server action stays in actionYes (degrades to tier 1)~15 KB
3. OptimisticuseActionState for pending UIYes (degrades to tier 1)~15 KB + React
// Dead before hydration
<form action={() => form.handleSubmit()}>

// Works immediately
<form action={createEntityAction}>

Three requirements for progressive forms:

RequirementWhatWhy
action={serverAction}Server action as form actionWorks before hydration
name on every inputServer reads FormDataNo client state required
redirect() in server actionNavigation after successNo client router required

Generator Enforcement

The form layer has been the consistent source of bugs. Discipline fails under pressure. Generators don't. An Nx generator scaffolds every new form with the correct pattern baked in — server action in action, name attributes on inputs, FormShell wrapping, Zod schema at the boundary.

What Generator ProducesWhy
Server action file with 4-step patternHex boundary enforced from first line
Zod schema for FormData parsingValidation at boundary, not in UI
FormShell with action={serverAction}Progressive enhancement by default
name attribute on every fieldServer reads FormData without client state
Integration test stub (server action)L2 test proves the action works without UI
E2E test stub (form submission)L3 test proves the form submits end-to-end

Companion lint rules:

RuleDetectsSeverity
no-client-only-form-actionaction={() => on <form> or <FormShell>Error
require-name-attribute<FormInput> or <Field> without nameError
require-server-action-importForm file without a 'use server' action importWarning

Run nx generate @stackmates/generators:form --name=contact --entity=contacts. The output compiles, the server action validates, the form submits before hydration. No manual wiring required.

This documents the WHAT/WHY spec for the generator. The engineering repo builds and maintains the actual generator. This page is the requirement for the generator's behavior.

Decision Rule

Start with native HTML forms and server actions. The action attribute takes a server action, not a client function. Add TanStack Form when you hit conditional fields, multi-step wizards, complex validation chains, or array fields. Even then, the server action stays in action. TanStack enhances — it does not replace.

Context

Questions

How does the action attribute determine whether a form works before hydration?

  • When does client-side validation justify its bundle cost over server-only validation?
  • What breaks when TanStack Form's handleSubmit replaces the server action in action?
  • How do multi-step forms interact with URL state and browser back?