TL;DR: TanStack Form ist die Formular-Bibliothek von den Machern von TanStack Query und TanStack Table. Sie bietet headless, typsichere Formular-Statusverwaltung mit integrierter Validierung, Field Arrays, asynchroner Validierung und erstklassiger TypeScript-Inferenz. Dieser Leitfaden behandelt alles – von der grundlegenden Einrichtung bis zu fortgeschrittenen Patterns für Produktions-SaaS-Anwendungen – einschließlich Server-Function-Integration, debounced Validierung, mehrstufigen Formularen und komplexen verschachtelten Feldstrukturen.
Warum TanStack Form?
React-Formularbibliotheken lassen sich traditionell in zwei Kategorien einteilen: vollständig kontrolliert (Formik, React Hook Form) oder render-prop-basiert. TanStack Form verfolgt einen anderen Ansatz – es ist eine headless State Machine für Formulare, die Ihnen vollständige Kontrolle über das Rendering gibt, während sie die komplexe Statusverwaltung übernimmt.
| Feature | TanStack Form | React Hook Form | Formik |
|---|---|---|---|
| Bundle-Größe | ~12 KB | ~10 KB | ~33 KB |
| TypeScript-Inferenz | End-to-End | Teilweise | Teilweise |
| Validierung | Integriert (sync/async) | Mit Zod/Yup | Mit Yup |
| Field Arrays | ✅ Nativ | ✅ useFieldArray | ✅ FieldArray |
| Asynchrone Validierung | ✅ Erstklassig | ✅ Mit Resolver | Manuell |
| Serverseitige Validierung | ✅ Nativ | Manuell | Manuell |
| Unkontrolliert | ✅ | ✅ | ❌ |
| React Native | ✅ | ✅ | ❌ |
Grundlegende Einrichtung
npm install @tanstack/react-form
Minimales Beispiel
import { useForm } from "@tanstack/react-form"
import { z } from "zod"
const signupSchema = z.object({
email: z.string().email("Ungültige E-Mail-Adresse"),
password: z.string().min(8, "Passwort muss mindestens 8 Zeichen lang sein"),
})
function SignupForm() {
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validators: {
onChange: signupSchema,
},
onSubmit: async ({ value }) => {
// value ist vollständig typisiert als { email: string; password: string }
await submitSignup(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="email"
children={(field) => (
<div>
<label>E-Mail</label>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span className="error">{field.state.meta.errors.join(", ")}</span>
)}
</div>
)}
/>
<form.Field
name="password"
children={(field) => (
<div>
<label>Passwort</label>
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span className="error">{field.state.meta.errors.join(", ")}</span>
)}
</div>
)}
/>
<button type="submit">Anmelden</button>
</form>
)
}
Die form.Field-Komponente bietet Ihnen eine Render-Prop mit vollständiger Typ-Inferenz. Der name-Prop wird gegen Ihre Default-Values-Struktur geprüft.
Validierungsstrategien
TanStack Form unterstützt mehrere Validierungsauslöser, die kombiniert werden können:
const form = useForm({
defaultValues: {
email: "",
age: 0,
},
validators: {
// Validierungsauslöser
onChange: z.object({
email: z.string().email(),
}),
onChangeAsync: z.object({
email: z.string().email().refine(async (email) => {
// Prüfen, ob E-Mail bereits registriert ist
const existing = await checkEmailExists(email)
return !existing
}, "E-Mail bereits registriert"),
}),
onChangeAsyncDebounceMs: 500, // Asynchrone Validierung verzögern
onBlur: z.object({
age: z.number().min(18, "Muss mindestens 18 Jahre alt sein"),
}),
onSubmit: z.object({
email: z.string().email(),
age: z.number().min(18),
}),
},
})
| Validator | Auslöser | Anwendungsfall |
|---|---|---|
onChange | Bei jeder Wertänderung | Sofortiges Feedback (Formatvalidierung) |
onChangeAsync | Bei Änderung, verzögert | Serverseitige Prüfungen (E-Mail-Eindeutigkeit) |
onBlur | Beim Verlassen des Feldes | Späte Validierung (komplexe Felder) |
onSubmit | Beim Formular-Absenden | Finale Validierung vor der Übermittlung |
Field Arrays
Für dynamische Formularfelder (z.B. Teammitglieder-Einladungen, Listenposten):
import { useFieldArray } from "@tanstack/react-form"
function TeamInviteForm() {
const form = useForm({
defaultValues: {
teamMembers: [{ email: "", role: "member" as const }],
},
})
return (
<form.Field name="teamMembers" mode="array">
{(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field key={i} name={`teamMembers[${i}].email`}>
{(subField) => (
<div className="flex gap-2">
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
placeholder="E-Mail-Adresse"
/>
<form.Field name={`teamMembers[${i}].role`}>
{(roleField) => (
<select
value={roleField.state.value}
onChange={(e) => roleField.handleChange(e.target.value as "member" | "admin")}
>
<option value="member">Mitglied</option>
<option value="admin">Admin</option>
</select>
)}
</form.Field>
<button onClick={() => field.removeValue(i)}>Entfernen</button>
</div>
)}
</form.Field>
))}
<button onClick={() => field.pushValue({ email: "", role: "member" })}>
Mitglied hinzufügen
</button>
</div>
)}
</form.Field>
)
}
Integration mit TanStack Start Server Functions
Die wahre Stärke von TanStack Form zeigt sich in Kombination mit TanStack Start Server Functions:
// server/auth.ts
import { createServerFn } from "@tanstack/react-start"
import { z } from "zod"
const signupSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
})
export const signupUser = createServerFn({ method: "POST" })
.validator((data: z.infer<typeof signupSchema>) => signupSchema.parse(data))
.handler(async ({ data }) => {
const existing = await env.DB.prepare(
"SELECT id FROM users WHERE email = ?"
).bind(data.email).first()
if (existing) {
// Strukturierten Validierungsfehler zurückgeben
return { success: false, fieldErrors: { email: "E-Mail bereits registriert" } }
}
const hashedPassword = await hashPassword(data.password)
await env.DB.prepare(
`INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)`
).bind(data.email, hashedPassword, data.name).run()
return { success: true }
})
// components/SignupForm.tsx
import { useForm } from "@tanstack/react-form"
import { useMutation } from "@tanstack/react-query"
import { signupUser } from "../server/auth"
function SignupForm() {
const mutation = useMutation({ mutationFn: (data) => signupUser({ data }) })
const form = useForm({
defaultValues: { email: "", password: "", name: "" },
validators: { onChange: signupSchema },
onSubmit: async ({ value }) => {
const result = await mutation.mutateAsync(value)
if (!result.success) {
// Serverseitige Validierungsfehler zuweisen
Object.entries(result.fieldErrors ?? {}).forEach(([field, message]) => {
form.setFieldMeta(field as any, (prev) => ({
...prev,
errors: [message],
}))
})
}
},
})
// ... Rendering
}
Mehrstufige Formulare
Für komplexe Abläufe wie Checkout oder Onboarding:
function MultiStepForm() {
const [step, setStep] = useState(0)
const form = useForm({
defaultValues: {
// Schritt 1: Konto
email: "",
password: "",
// Schritt 2: Profil
name: "",
company: "",
// Schritt 3: Abrechnung
plan: "starter" as const,
couponCode: "",
},
validators: {
onBlur: z.object({
email: z.string().email(),
password: z.string().min(8),
}),
onSubmit: z.object({
email: z.string().email(),
password: z.string().min(8),
}),
onChange: (values) => {
switch (step) {
case 0:
return z.object({
email: z.string().email(),
password: z.string().min(8),
}).safeParse(values).success
case 1:
return z.object({
name: z.string().min(2),
company: z.string().min(1),
}).safeParse(values).success
default:
return true
}
},
},
})
return (
<div>
{step === 0 && <AccountStep form={form} />}
{step === 1 && <ProfileStep form={form} />}
{step === 2 && <BillingStep form={form} />}
<div className="flex justify-between">
{step > 0 && <button onClick={() => setStep(step - 1)}>Zurück</button>}
{step < 2 ? (
<button onClick={() => setStep(step + 1)}>Weiter</button>
) : (
<button onClick={() => form.handleSubmit()}>Absenden</button>
)}
</div>
</div>
)
}
Barrierefreiheit und benutzerdefinierte Komponenten
TanStack Form ist headless – Sie bringen Ihre eigene UI mit. Hier ist ein barrierefreier Wrapper:
function FormField<T extends FieldApi<any, any, any, any>>({
field,
label,
children,
}: {
field: T
label: string
children: (field: T) => React.ReactNode
}) {
const fieldId = `field-${field.name}`
const errorId = `error-${field.name}`
return (
<div role="group" aria-labelledby={fieldId}>
<label id={fieldId} htmlFor={fieldId}>
{label}
</label>
{children(field)}
{field.state.meta.errors && (
<span id={errorId} role="alert" className="text-red-500 text-sm">
{field.state.meta.errors.join(", ")}
</span>
)}
</div>
)
}
Performance: Feldbezogene Re-Renderings
TanStack Form isoliert Re-Renderings auf einzelne Felder. Wenn sich ein Feld ändert, wird nur die Komponente dieses Felds neu gerendert – nicht das gesamte Formular. Dies ist entscheidend für Formulare mit vielen Feldern:
// Jede Field-Komponente abonniert nur eigene Statusänderungen
// Änderung von "email" rendert nicht das "password"-Feld neu
<form.Field name="email">
{(field) => <input value={field.state.value} onChange={...} />}
</form.Field>
<form.Field name="password">
{/* Diese Komponente rendert nicht neu, wenn sich E-Mail ändert */}
{(field) => <input value={field.state.value} onChange={...} />}
</form.Field>
Checkliste: Produktionsreife Formularvalidierung
- Alle benutzerorientierten Formulare nutzen
onChange-Validierung für sofortiges Feedback - Asynchrone Validierung ist verzögert (300–500 ms), um übermäßige Server-Aufrufe zu vermeiden
- Serverseitige Validierungsfehler werden auf Formularfelder abgebildet
- Formularstatus bleibt erhalten, wenn der Nutzer weg navigiert (localStorage oder Session)
- Barrierefreie Fehlermeldungen mit
role="alert"undaria-describedby - Absenden-Button zeigt Ladezustand während der Übermittlung
- Doppelsubmission verhindern (Button nach erstem Klick deaktivieren)
- Formulardaten werden auf dem Server validiert (nie nur clientseitige Validierung vertrauen)
- Field Arrays haben ordnungsgemäße Schlüsselverwaltung für dynamisches Hinzufügen/Entfernen
- Mehrstufige Formulare zeigen Fortschritt und erlauben Rückschritt
Fazit
TanStack Form ist nicht nur eine weitere Formularbibliothek – es ist ein grundlegender Baustein des typsicheren React-Stacks. In Kombination mit TanStack Start Server Functions und TanStack Query Mutations bietet es ein Formularerlebnis, bei dem Typen nahtlos von der Datenbankschema bis zum Input-Feld fließen – ohne manuelle Verkabelung.
Die wichtigsten Punkte:
- Nutzen Sie
onChange+onChangeAsynczusammen — synchrone Validierung für Format, async für Serverseitige Prüfungen - Validieren Sie auf dem Server mit
createServerFnund Zod-Schemas, dann bilden Sie Fehler auf Felder ab - Nutzen Sie Field Arrays für dynamische Inhalte, anstatt Array-Status manuell zu verwalten
- Behalten Sie Formulare headless — TanStack Form kümmert sich um den Status; Sie kümmern sich um das Design
Für tiefergehende Informationen, lesen Sie TanStack Query + Server Functions: Integration Patterns für die Datenebene, die mit Ihren Formularen zusammenarbeitet. Für die Anzeige von Formulardaten in Datentabellen, deckt Building Data-Rich SaaS Interfaces with TanStack Table die Tabellenintegration ab. Der TanStack Ecosystem Overview bietet Kontext, wie alle Bibliotheken zusammenarbeiten, und der TanStack Virtual Scrolling Guide ist nützlich für Formulare mit großen Datensätzen.