# React Hook Form und Zod: Ein Jahr in der Produktion ohne Boilerplate-Code oder versteckte Fallstricke
Nach einem Jahr intensiver Nutzung der Kombination aus React Hook Form und Zod in der Produktion bei einem großen Projekt bestätigt das Team: Der deklarative Ansatz für Formulare reduziert das Codevolumen um 30–40 %, erfordert aber Aufmerksamkeit bei den Implementierungsdetails. Wir teilen die Fallstricke, die wir entdeckt haben, und wie man sie umgeht.
Von MobX zu deklarativen Formularen: Warum den Ansatz wechseln?
In früheren Projektiterationen wurden Formulare im MVC-Stil mit MobX erstellt. Diese Methode ermöglichte die Steuerung des Zustands, erzeugte aber Boilerplate-Code: Jedes Feld benötigte separate Methoden im Controller, Fehler-Arrays und manuelle Synchronisation mit der UI. Zum Beispiel bedeutete das Hinzufügen eines neuen Feldes das Erstellen von:
export class SomeController {
public setDuration = (v: number) => {
this.store.duration = v;
}
}
Solcher Vorlagen-Code hatte nichts mit der Geschäftslogik zu tun, nahm aber bis zu 60 % des Volumens der Komponente ein. Der Wechsel zu React Hook Form (RHF) mit Zod löste das Problem: Das Validierungsschema wird deklarativ beschrieben, und die Bibliothek übernimmt die automatische Zustandssynchronisation und Fehlerbehandlung.
Beispiel für ein einfaches Formular:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
name: z.string().min(1, 'Enter name'),
email: z.string().email('Invalid email'),
age: z.coerce.number().min(0, 'Invalid vozrast').optional(),
});
type FormValues = z.infer<typeof formSchema>;
export function ExampleForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
},
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<div>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input type="number" {...register('age')} />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit">Send</button>
</form>
);
}
Ergebnis: Das Codevolumen schrumpfte, die Wartung wurde einfacher, und die Entwicklungsgeschwindigkeit neuer Formulare stieg. Allerdings tauchten bei der Praxisnutzung Nuancen auf, die in der Dokumentation nicht abgedeckt sind.
Drei Fallstricke bei der Kombination aus React Hook Form und Zod
In über einem Jahr Nutzung von RHF und Zod stießen wir auf drei zentrale Probleme, die die Formularfunktionalität in bestimmten Szenarien stören können.
1. Umgang mit undefined und null
RHF unterstützt undefined nicht als Feldwert. Der Versuch:
methods.setValue('age', undefined)
führt dazu, dass das Feld die Änderung ignoriert. Die Dokumentation erwähnt das, aber es fällt nicht sofort ins Auge. Der Standardfix ist die Verwendung von null und die Deklaration des Feldes als .nullable():
const schema = z.object({
age: z.number().nullable()
});
methods.setValue('age', null);
Allerdings kann das mit benutzerdefinierten Komponenten kollidieren, die eine Zahl erwarten. Zum Beispiel könnte eine Eingabe-Komponente für das Alter bei null versagen. Umgekehrt gibt die Komponente bei Löschen undefined zurück, aber RHF verarbeitet diesen Wert nicht.
Reset-Buttons erhöhen die Komplexität zusätzlich. Die reset()-Methode holt Werte aus defaultValues, aber wenn diese auf undefined gesetzt sind, schlägt reset fehl. Lösung: Verwende null oder einen leeren String für String-Felder.
2. Unerwartete Typfehler in Zod
Das Problem tritt bei Typkonflikten auf. Zum Beispiel dieses Schema:
const schema = z.object({
field1: z.string().min(1, 'Fill field').optional()
});
Wenn die Komponente null (nicht undefined) zurückgibt, wirft Zod invalid type (expected string received null). Benutzer sehen eine Systemmeldung statt deiner benutzerdefinierten Nachricht.
Lösung: Explizit Fehlermeldungen für alle Validierungsschritte setzen:
const schema = z.object({
field1: z.string('Fill field').min(1, 'Fill field').optional()
});
Das stellt sicher, dass bei jedem Fehler deine Meldung erscheint.
3. Instabilität des isDirty-Flags
Das formState.isDirty-Flag zeigt an, ob das Formular Änderungen aufweist. Beim Initialisieren mit leeren defaultValues jedoch:
const methods = useForm({
resolver: zodResolver(formSchema),
defaultValues: {},
});
wird isDirty direkt nach dem Rendering zu true, auch wenn der Benutzer nichts angefasst hat. dirtyFields bleibt derweil ein leeres Objekt.
Das rührt von einer Abweichung zwischen der Struktur von defaultValues und dem Schema her. Um falsche Auslöser zu vermeiden, stelle sicher, dass defaultValues alle Felder des Schemas enthält – auch mit undefined oder null.
Wie vermeidet man gängige Fehler?
Aus der Erfahrung: Hier bewährte Praktiken:
- Zum Umgang mit leeren Werten:
- String-Felder: Verwende einen leeren String als Default.
- Numerische/Objekt-Felder: Wende .nullable() in Zod an und null für Werte.
- Überprüfe immer, ob benutzerdefinierte Komponenten den gewählten Typ unterstützen.
- Für zuverlässige Validierung:
- Gib eine allgemeine Fehlermeldung für jeden Validierungsschritt an, wie im obigen Beispiel.
- Teste null- und undefined-Szenarien während der Komponentenentwicklung.
- Für korrektes isDirty-Verhalten:
- Initialisiere defaultValues passend zum Schema und decke alle Felder ab.
- Vermeide das leere Objekt {} – liste Felder explizit mit undefined oder null auf.
Wir empfehlen auch, einheitliche Helfer für Formulare ins Projekt zu integrieren, wie eine Reset-Funktion, die die Eigenarten von RHF berücksichtigt:
const resetForm = () => {
methods.reset({
name: null,
email: null,
age: null,
});
};
Wichtige Erkenntnisse
- Code-Reduktion macht Tests nicht überflüssig: Der deklarative Ansatz spart Zeit, erfordert aber gründliche Überprüfungen von Randfällen.
- Typensicherheit ist dein Freund: Zod mit TypeScript verhindert viele Bugs, aber Schemas müssen korrekt eingerichtet werden.
- Dokumentiert die Eigenarten: Protokolliert Workarounds für gängige RHF- und Zod-Probleme in den internen Projekt-Docs.
— Editorial Team
Noch keine Kommentare.