TanStack Form Complete Guide: Type-Sichere Formularvalidierung in React

Ein umfassender Leitfaden zu TanStack Form für React – von Formular-Statusverwaltung über Validierungsstrategien, Field Arrays und asynchrone Validierung bis zur Integration mit TanStack Query und Server Functions.

Sam Rivera
Sam Rivera
16. Juni 202614 min read
Auch verfügbar auf:English · 中文

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.

FeatureTanStack FormReact Hook FormFormik
Bundle-Größe~12 KB~10 KB~33 KB
TypeScript-InferenzEnd-to-EndTeilweiseTeilweise
ValidierungIntegriert (sync/async)Mit Zod/YupMit Yup
Field Arrays✅ Nativ✅ useFieldArray✅ FieldArray
Asynchrone Validierung✅ Erstklassig✅ Mit ResolverManuell
Serverseitige Validierung✅ NativManuellManuell
Unkontrolliert✅✅❌
React Native✅✅❌

Grundlegende Einrichtung

bash
npm install @tanstack/react-form

Minimales Beispiel

typescript
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:

typescript
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),
    }),
  },
})
ValidatorAuslöserAnwendungsfall
onChangeBei jeder WertänderungSofortiges Feedback (Formatvalidierung)
onChangeAsyncBei Änderung, verzögertServerseitige Prüfungen (E-Mail-Eindeutigkeit)
onBlurBeim Verlassen des FeldesSpäte Validierung (komplexe Felder)
onSubmitBeim Formular-AbsendenFinale Validierung vor der Übermittlung

Field Arrays

Für dynamische Formularfelder (z.B. Teammitglieder-Einladungen, Listenposten):

typescript
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:

typescript
// 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 }
  })
typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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" und aria-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:

  1. Nutzen Sie onChange + onChangeAsync zusammen — synchrone Validierung für Format, async für Serverseitige Prüfungen
  2. Validieren Sie auf dem Server mit createServerFn und Zod-Schemas, dann bilden Sie Fehler auf Felder ab
  3. Nutzen Sie Field Arrays für dynamische Inhalte, anstatt Array-Status manuell zu verwalten
  4. 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.