Files
Portfolio/blog-posts/38-react-hook-form.md
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

575 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# React Hook Form: Performante Formulare mit TypeScript
**Meta-Description:** React Hook Form Masterguide für 2026. Performance-Optimierung, TypeScript-Patterns, Zod-Integration und Server Actions Support.
**Keywords:** React Hook Form, Form Validation, TypeScript Forms, Zod Integration, Server Actions, React Forms, Uncontrolled Components
---
## Einführung
React Hook Form ist der **Gold-Standard für Formulare in React 2026**. Durch Uncontrolled Components und Ref-basiertes State Management erreicht es minimale Re-Renders selbst bei Formularen mit hunderten Feldern.
---
## Warum React Hook Form?
```
┌─────────────────────────────────────────────────────────────┐
│ REACT HOOK FORM VORTEILE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Performance: │
│ ├── Uncontrolled Components (keine Re-Renders) │
│ ├── Ref-basiertes State Management │
│ ├── Isolierte Input-Subscriptions │
│ └── 100+ Felder ohne Lag │
│ │
│ Developer Experience: │
│ ├── Minimale API │
│ ├── First-Class TypeScript Support │
│ ├── Tree-Shakeable (~8KB gzipped) │
│ └── Keine Dependencies │
│ │
│ 2026 Features: │
│ ├── Server Actions Integration │
│ ├── useActionState Support │
│ └── useFormStatus Hook │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Basic Setup
```typescript
import { useForm } from 'react-hook-form';
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
function LoginPage() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
defaultValues: {
email: '',
password: '',
rememberMe: false
}
});
const onSubmit = async (data: LoginForm) => {
console.log(data); // Vollständig typisiert!
await login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'E-Mail ist erforderlich',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Ungültige E-Mail-Adresse'
}
})}
type="email"
placeholder="E-Mail"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
{...register('password', {
required: 'Passwort ist erforderlich',
minLength: {
value: 8,
message: 'Mindestens 8 Zeichen'
}
})}
type="password"
placeholder="Passwort"
/>
{errors.password && <span>{errors.password.message}</span>}
<label>
<input {...register('rememberMe')} type="checkbox" />
Angemeldet bleiben
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Lädt...' : 'Anmelden'}
</button>
</form>
);
}
```
---
## Zod Integration
```typescript
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Schema definieren
const SignupSchema = z.object({
name: z.string()
.min(2, 'Name zu kurz')
.max(50, 'Name zu lang'),
email: z.string()
.email('Ungültige E-Mail'),
password: z.string()
.min(8, 'Mindestens 8 Zeichen')
.regex(/[A-Z]/, 'Mindestens ein Großbuchstabe')
.regex(/[0-9]/, 'Mindestens eine Zahl'),
confirmPassword: z.string(),
terms: z.literal(true, {
errorMap: () => ({ message: 'Sie müssen die AGB akzeptieren' })
})
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwörter stimmen nicht überein',
path: ['confirmPassword']
});
// Type aus Schema inferieren
type SignupData = z.infer<typeof SignupSchema>;
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<SignupData>({
resolver: zodResolver(SignupSchema),
mode: 'onBlur' // Validierung bei Blur
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="Name" />
{errors.name && <p>{errors.name.message}</p>}
<input {...register('email')} type="email" placeholder="E-Mail" />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} type="password" placeholder="Passwort" />
{errors.password && <p>{errors.password.message}</p>}
<input {...register('confirmPassword')} type="password" placeholder="Passwort wiederholen" />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<label>
<input {...register('terms')} type="checkbox" />
AGB akzeptieren
</label>
{errors.terms && <p>{errors.terms.message}</p>}
<button type="submit">Registrieren</button>
</form>
);
}
```
---
## Controller für UI Libraries
```typescript
import { useForm, Controller } from 'react-hook-form';
import { Select, DatePicker, Switch } from './ui-library';
interface ProfileForm {
country: string;
birthDate: Date;
newsletter: boolean;
}
function ProfileForm() {
const { control, handleSubmit } = useForm<ProfileForm>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Select Component */}
<Controller
name="country"
control={control}
rules={{ required: 'Land ist erforderlich' }}
render={({ field, fieldState }) => (
<Select
{...field}
options={countries}
error={fieldState.error?.message}
/>
)}
/>
{/* Date Picker */}
<Controller
name="birthDate"
control={control}
render={({ field }) => (
<DatePicker
selected={field.value}
onChange={field.onChange}
maxDate={new Date()}
/>
)}
/>
{/* Custom Switch */}
<Controller
name="newsletter"
control={control}
render={({ field }) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<button type="submit">Speichern</button>
</form>
);
}
```
---
## useFormContext für Nested Components
```typescript
import { useForm, FormProvider, useFormContext } from 'react-hook-form';
interface CheckoutForm {
shipping: {
name: string;
address: string;
city: string;
};
billing: {
cardNumber: string;
expiry: string;
cvv: string;
};
}
// Parent Form
function CheckoutPage() {
const methods = useForm<CheckoutForm>();
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<ShippingSection />
<BillingSection />
<button type="submit">Bestellen</button>
</form>
</FormProvider>
);
}
// Child Component mit typisiertem Context
function ShippingSection() {
const { register, formState: { errors } } = useFormContext<CheckoutForm>();
return (
<fieldset>
<legend>Versand</legend>
<input {...register('shipping.name')} placeholder="Name" />
{errors.shipping?.name && <span>{errors.shipping.name.message}</span>}
<input {...register('shipping.address')} placeholder="Adresse" />
<input {...register('shipping.city')} placeholder="Stadt" />
</fieldset>
);
}
function BillingSection() {
const { register } = useFormContext<CheckoutForm>();
return (
<fieldset>
<legend>Zahlung</legend>
<input {...register('billing.cardNumber')} placeholder="Kartennummer" />
<input {...register('billing.expiry')} placeholder="MM/YY" />
<input {...register('billing.cvv')} placeholder="CVV" />
</fieldset>
);
}
```
---
## Field Arrays (Dynamische Felder)
```typescript
import { useForm, useFieldArray } from 'react-hook-form';
interface OrderForm {
items: {
productId: string;
quantity: number;
price: number;
}[];
}
function OrderForm() {
const { control, register, handleSubmit, watch } = useForm<OrderForm>({
defaultValues: {
items: [{ productId: '', quantity: 1, price: 0 }]
}
});
const { fields, append, remove, move } = useFieldArray({
control,
name: 'items'
});
const watchItems = watch('items');
const total = watchItems.reduce((sum, item) =>
sum + (item.quantity * item.price), 0
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<select {...register(`items.${index}.productId`)}>
{products.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
type="number"
min={1}
/>
<input
{...register(`items.${index}.price`, { valueAsNumber: true })}
type="number"
step="0.01"
/>
<button type="button" onClick={() => remove(index)}>
Entfernen
</button>
</div>
))}
<button
type="button"
onClick={() => append({ productId: '', quantity: 1, price: 0 })}
>
Produkt hinzufügen
</button>
<p>Gesamt: {total.toFixed(2)} </p>
<button type="submit">Bestellen</button>
</form>
);
}
```
---
## Server Actions Integration (Next.js 15)
```typescript
// app/actions.ts
'use server';
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10)
});
export async function submitContact(formData: FormData) {
const data = ContactSchema.parse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message')
});
await sendEmail(data);
return { success: true };
}
// app/contact/page.tsx
'use client';
import { useForm } from 'react-hook-form';
import { useActionState } from 'react';
import { submitContact } from './actions';
function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const [state, formAction, isPending] = useActionState(submitContact, null);
return (
<form action={formAction}>
<input {...register('name')} name="name" />
<input {...register('email')} name="email" type="email" />
<textarea {...register('message')} name="message" />
<button type="submit" disabled={isPending}>
{isPending ? 'Wird gesendet...' : 'Absenden'}
</button>
{state?.success && <p>Nachricht gesendet!</p>}
</form>
);
}
```
---
## Performance-Optimierung
```typescript
import { useForm, useWatch } from 'react-hook-form';
import { memo } from 'react';
// 1. Isolierte Watch für einzelne Felder
function PriceDisplay({ control }: { control: Control<FormData> }) {
// Nur dieses Feld subscriben
const price = useWatch({ control, name: 'price' });
return <span>{price} </span>;
}
// 2. Memoized Components
const ExpensiveField = memo(function ExpensiveField({
register,
name
}: {
register: UseFormRegister<FormData>;
name: keyof FormData;
}) {
return <input {...register(name)} />;
});
// 3. Mode-Strategien
const { register } = useForm({
mode: 'onBlur', // Validierung nur bei Blur
reValidateMode: 'onChange', // Re-Validierung bei Change
shouldFocusError: true, // Fokus auf erstes Fehlerfeld
criteriaMode: 'firstError' // Nur erster Fehler pro Feld
});
// 4. Verzögerte Validierung
const { register } = useForm({
delayError: 500 // Fehleranzeige um 500ms verzögern
});
```
---
## Error Handling Patterns
```typescript
import { useForm, FieldErrors } from 'react-hook-form';
// Globales Error Display
function ErrorSummary({ errors }: { errors: FieldErrors }) {
const errorMessages = Object.entries(errors)
.filter(([_, error]) => error?.message)
.map(([field, error]) => ({
field,
message: error?.message as string
}));
if (errorMessages.length === 0) return null;
return (
<div role="alert" className="error-summary">
<h3>Bitte korrigieren Sie folgende Fehler:</h3>
<ul>
{errorMessages.map(({ field, message }) => (
<li key={field}>{message}</li>
))}
</ul>
</div>
);
}
// Server Error Integration
function FormWithServerErrors() {
const {
setError,
clearErrors,
formState: { errors }
} = useForm();
const onSubmit = async (data: FormData) => {
try {
await submitToServer(data);
} catch (error) {
if (error instanceof ValidationError) {
// Server-Errors in Form setzen
error.fields.forEach(({ name, message }) => {
setError(name, { type: 'server', message });
});
} else {
// Globaler Error
setError('root', {
type: 'server',
message: 'Ein unerwarteter Fehler ist aufgetreten'
});
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{errors.root && <div className="global-error">{errors.root.message}</div>}
{/* ... */}
</form>
);
}
```
---
## Fazit
React Hook Form bietet:
1. **Maximale Performance**: Uncontrolled Components, minimale Re-Renders
2. **TypeScript-First**: Volle Type-Safety ohne Aufwand
3. **Flexible Validation**: Zod, Yup, Superstruct Integration
4. **Server Actions Ready**: Perfekte Integration mit Next.js 15
Für jedes React-Projekt 2026 ist React Hook Form die erste Wahl.
---
## Bildprompts
1. "Form inputs with performance metrics overlay, minimal re-render visualization"
2. "TypeScript code flowing into form components, type safety concept"
3. "Server and client form validation synchronization, full-stack concept"
---
## Quellen
- [React Hook Form Documentation](https://react-hook-form.com/)
- [React Hook Form TypeScript Support](https://www.react-hook-form.com/ts/)
- [Zod Resolver](https://react-hook-form.com/get-started#SchemaValidation)
- [Best React Form Libraries 2026](https://blog.croct.com/post/best-react-form-libraries)