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>
575 lines
15 KiB
Markdown
575 lines
15 KiB
Markdown
# 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)
|