Formulare sind in React komplizierter als in klassischem HTML. Ein
<input> hat normalerweise seinen eigenen State – der
Nutzer tippt, der Wert ändert sich. React will aber State kontrollieren.
Diese Spannung zwischen DOM-State und React-State führt zu zwei
grundlegend verschiedenen Ansätzen: Controlled und Uncontrolled
Components.
In HTML verwaltet das DOM den Form-State:
<!-- Traditionelles HTML -->
<form>
<input type="text" name="email" />
<button type="submit">Submit</button>
</form>
<script>
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const email = form.email.value; // DOM ist Source of Truth
console.log(email);
});
</script>In React gibt es zwei Philosophien:
Controlled Components: React-State ist die Single Source of Truth. Der Input-Wert kommt aus State, jede Änderung updated den State.
Uncontrolled Components: Das DOM behält die Kontrolle. React greift nur über Refs auf Werte zu.
Bei Controlled Components synchronisiert React den Input-Wert permanent mit State.
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log({ email, password });
// email und password sind immer aktuell in State
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email} // State bindet Value
onChange={(e) => setEmail(e.target.value)} // Jede Änderung updated State
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
);
}
Vorteile: - State ist jederzeit verfügbar (nicht nur bei Submit) - Einfache Validierung während der Eingabe - Bedingte Rendering basierend auf Input-Werten - Easy Reset/Prefill durch State-Änderung
Nachteile: - Re-Render bei jedem Tastendruck - Mehr Boilerplate (State + onChange für jedes Feld) - Performance-Probleme bei vielen Feldern
React’s Synthetic Events sind typisiert. Wichtig: Die Typen unterscheiden sich je nach Element.
// onChange Event-Typen
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value; // string
const checked = e.target.checked; // boolean (nur bei checkbox/radio)
}
function handleTextareaChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const value = e.target.value;
}
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
const value = e.target.value;
}
// Submit Event
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
}
// Focus Events
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
console.log('Input focused');
}
function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
console.log('Input blurred');
}
// Keyboard Events
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
}
Generisches Event-Handler-Pattern für wiederverwendbare Handler:
function useFormState<T extends Record<string, any>>(initialState: T) {
const [values, setValues] = useState<T>(initialState);
// Generischer Change-Handler
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
// Checkbox-Handling
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setValues(prev => ({ ...prev, [name]: checked }));
} else {
setValues(prev => ({ ...prev, [name]: value }));
}
};
const reset = () => setValues(initialState);
return { values, handleChange, reset };
}
// Verwendung
function RegistrationForm() {
const { values, handleChange, reset } = useFormState({
email: '',
password: '',
newsletter: false
});
return (
<form>
<input
name="email"
type="email"
value={values.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
<label>
<input
name="newsletter"
type="checkbox"
checked={values.newsletter}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
<button type="button" onClick={reset}>Reset</button>
</form>
);
}
Jeder Input-Typ braucht leicht andere Handling:
function AllInputTypes() {
const [formData, setFormData] = useState({
text: '',
email: '',
number: 0,
date: '',
checkbox: false,
radio: 'option1',
select: '',
multiSelect: [] as string[],
textarea: ''
});
return (
<form>
{/* Text Input */}
<input
type="text"
value={formData.text}
onChange={(e) => setFormData({ ...formData, text: e.target.value })}
/>
{/* Email Input */}
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
{/* Number Input */}
<input
type="number"
value={formData.number}
onChange={(e) => setFormData({ ...formData, number: Number(e.target.value) })}
/>
{/* Date Input */}
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
/>
{/* Checkbox */}
<input
type="checkbox"
checked={formData.checkbox}
onChange={(e) => setFormData({ ...formData, checkbox: e.target.checked })}
/>
{/* Radio Buttons */}
<label>
<input
type="radio"
value="option1"
checked={formData.radio === 'option1'}
onChange={(e) => setFormData({ ...formData, radio: e.target.value })}
/>
Option 1
</label>
<label>
<input
type="radio"
value="option2"
checked={formData.radio === 'option2'}
onChange={(e) => setFormData({ ...formData, radio: e.target.value })}
/>
Option 2
</label>
{/* Select (Single) */}
<select
value={formData.select}
onChange={(e) => setFormData({ ...formData, select: e.target.value })}
>
<option value="">Select...</option>
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
{/* Select (Multiple) */}
<select
multiple
value={formData.multiSelect}
onChange={(e) => {
const selected = Array.from(e.target.selectedOptions, option => option.value);
setFormData({ ...formData, multiSelect: selected });
}}
>
<option value="x">X</option>
<option value="y">Y</option>
<option value="z">Z</option>
</select>
{/* Textarea */}
<textarea
value={formData.textarea}
onChange={(e) => setFormData({ ...formData, textarea: e.target.value })}
/>
</form>
);
}
| Input-Typ | Value-Prop | Event-Property | Typ |
|---|---|---|---|
text, email, password |
value |
e.target.value |
string |
number |
value |
Number(e.target.value) |
number |
checkbox |
checked |
e.target.checked |
boolean |
radio |
checked |
e.target.value |
string |
select |
value |
e.target.value |
string |
select[multiple] |
value |
Array.from(e.target.selectedOptions) |
string[] |
textarea |
value |
e.target.value |
string |
file |
- | e.target.files |
FileList |
Bei Uncontrolled Components verwaltet das DOM den State. React liest Werte nur bei Bedarf über Refs.
import { useRef } from 'react';
function UncontrolledLoginForm() {
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Werte nur bei Submit aus DOM lesen
const email = emailRef.current?.value;
const password = passwordRef.current?.value;
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
ref={emailRef}
type="email"
defaultValue="" // defaultValue statt value!
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
ref={passwordRef}
type="password"
defaultValue=""
/>
</div>
<button type="submit">Login</button>
</form>
);
}
Vorteile: - Weniger Code (kein State, kein onChange) - Keine Re-Renders bei Eingabe - Gut für einfache Formulare oder Third-Party-Integration
Nachteile: - Keine Live-Validierung - State nicht verfügbar für bedingtes Rendering - Schwieriger zu testen (DOM-abhängig) - Kein programmatisches Reset ohne Ref-Manipulation
// ❌ Falsch: value ohne onChange
<input type="text" value="fixed" />
// React Warning: "You provided a `value` prop without an `onChange` handler"
// Input ist read-only!
// ✓ Richtig: Controlled mit onChange
<input
type="text"
value={state}
onChange={(e) => setState(e.target.value)}
/>
// ✓ Richtig: Uncontrolled mit defaultValue
<input
type="text"
defaultValue="initial"
ref={inputRef}
/>
// Nutzer kann tippen, React liest Wert später via Ref
Regel: Niemals value und
defaultValue gleichzeitig verwenden!
Browser bieten eingebaute Validation:
function NativeValidationForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Validation erfolgt automatisch durch Browser
const formData = new FormData(e.currentTarget);
console.log(Object.fromEntries(formData));
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
required
placeholder="you@example.com"
/>
<input
type="password"
name="password"
required
minLength={8}
pattern="^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$"
title="Mindestens 8 Zeichen, Buchstaben und Zahlen"
/>
<input
type="url"
name="website"
pattern="https?://.+"
/>
<input
type="number"
name="age"
min={18}
max={120}
/>
<button type="submit">Submit</button>
</form>
);
}
Vorteile: Zero JS, funktioniert ohne React
Nachteile: Styling begrenzt, Fehlermeldungen nicht
customizable, kein granulares Control
Für volle Kontrolle: Manuelle Validierung mit State.
interface FormErrors {
email?: string;
password?: string;
}
function CustomValidationForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const validateEmail = (value: string): string | undefined => {
if (!value) return 'Email ist erforderlich';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Ungültige Email-Adresse';
}
};
const validatePassword = (value: string): string | undefined => {
if (!value) return 'Passwort ist erforderlich';
if (value.length < 8) return 'Mindestens 8 Zeichen';
if (!/(?=.*[A-Za-z])(?=.*\d)/.test(value)) {
return 'Muss Buchstaben und Zahlen enthalten';
}
};
const handleBlur = (field: keyof FormErrors) => {
setTouched(prev => ({ ...prev, [field]: true }));
// Validierung bei Blur
const newErrors: FormErrors = { ...errors };
if (field === 'email') {
const error = validateEmail(email);
if (error) newErrors.email = error;
else delete newErrors.email;
}
if (field === 'password') {
const error = validatePassword(password);
if (error) newErrors.password = error;
else delete newErrors.password;
}
setErrors(newErrors);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Alle Felder validieren
const emailError = validateEmail(email);
const passwordError = validatePassword(password);
if (emailError || passwordError) {
setErrors({
email: emailError,
password: passwordError
});
setTouched({ email: true, password: true });
return;
}
// Form ist valid
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => handleBlur('email')}
aria-invalid={touched.email && !!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{touched.email && errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onBlur={() => handleBlur('password')}
aria-invalid={touched.password && !!errors.password}
/>
{touched.password && errors.password && (
<span role="alert" className="error">
{errors.password}
</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
Pattern: touched-State verhindert, dass
Fehler sofort beim Laden angezeigt werden – erst nach
User-Interaktion.
interface ValidationRules<T> {
[key: string]: (value: any, formData: T) => string | undefined;
}
function useFormValidation<T extends Record<string, any>>(
initialValues: T,
rules: ValidationRules<T>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const validate = (field?: keyof T): boolean => {
const fieldsToValidate = field ? [field] : Object.keys(rules);
const newErrors: Partial<Record<keyof T, string>> = { ...errors };
fieldsToValidate.forEach(key => {
const rule = rules[key];
if (rule) {
const error = rule(values[key], values);
if (error) {
newErrors[key as keyof T] = error;
} else {
delete newErrors[key as keyof T];
}
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }));
// Live-Validation für bereits berührte Felder
if (touched[name]) {
setTimeout(() => validate(name), 0);
}
};
const handleBlur = (name: keyof T) => {
setTouched(prev => ({ ...prev, [name]: true }));
validate(name);
};
const handleSubmit = (onValid: (values: T) => void) => {
return (e: React.FormEvent) => {
e.preventDefault();
// Alle als touched markieren
const allTouched = Object.keys(rules).reduce((acc, key) => ({
...acc,
[key]: true
}), {});
setTouched(allTouched);
if (validate()) {
onValid(values);
}
};
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit
};
}
// Verwendung
function RegistrationForm() {
const { values, errors, touched, handleChange, handleBlur, handleSubmit } =
useFormValidation(
{ email: '', password: '', confirmPassword: '' },
{
email: (value) => {
if (!value) return 'Email erforderlich';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Ungültige Email';
},
password: (value) => {
if (!value) return 'Passwort erforderlich';
if (value.length < 8) return 'Mindestens 8 Zeichen';
},
confirmPassword: (value, formData) => {
if (value !== formData.password) return 'Passwörter stimmen nicht überein';
}
}
);
return (
<form onSubmit={handleSubmit(console.log)}>
<div>
<input
type="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
type="password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
</div>
<div>
<input
type="password"
value={values.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
onBlur={() => handleBlur('confirmPassword')}
/>
{touched.confirmPassword && errors.confirmPassword && (
<span>{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Register</button>
</form>
);
}
Für komplexe Formulare ist React Hook Form der aktuelle Standard. Performant, wenig Boilerplate, TypeScript-Integration.
npm install react-hook-formimport { useForm } from 'react-hook-form';
interface FormData {
email: string;
password: string;
age: number;
newsletter: boolean;
}
function ReactHookFormExample() {
const {
register, // Registriert Input-Felder
handleSubmit, // Wrapper für Submit-Handler
formState: { errors, isSubmitting } // Validation-State
} = useForm<FormData>({
defaultValues: {
email: '',
password: '',
age: 0,
newsletter: false
}
});
const onSubmit = async (data: FormData) => {
console.log(data);
// API Call hier
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email', {
required: 'Email ist erforderlich',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Ungültige Email-Adresse'
}
})}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input
{...register('password', {
required: 'Passwort erforderlich',
minLength: {
value: 8,
message: 'Mindestens 8 Zeichen'
}
})}
type="password"
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>
<div>
<input
{...register('age', {
valueAsNumber: true,
min: { value: 18, message: 'Mindestalter 18' }
})}
type="number"
placeholder="Age"
/>
{errors.age && <span>{errors.age.message}</span>}
</div>
<label>
<input {...register('newsletter')} type="checkbox" />
Subscribe to newsletter
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
import { useForm } from 'react-hook-form';
interface RegisterFormData {
username: string;
email: string;
password: string;
confirmPassword: string;
}
function RegistrationForm() {
const { register, handleSubmit, formState: { errors }, watch } = useForm<RegisterFormData>();
const password = watch('password'); // Watch andere Felder für Validation
const onSubmit = (data: RegisterFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('username', {
required: 'Username erforderlich',
minLength: { value: 3, message: 'Mindestens 3 Zeichen' },
validate: async (value) => {
// Async Validation: Prüfe ob Username verfügbar
const response = await fetch(`/api/check-username?username=${value}`);
const available = await response.json();
return available || 'Username bereits vergeben';
}
})}
placeholder="Username"
/>
{errors.username && <span>{errors.username.message}</span>}
</div>
<div>
<input
{...register('email', {
required: 'Email erforderlich',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Ungültige Email'
}
})}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input
{...register('password', {
required: 'Passwort erforderlich',
minLength: { value: 8, message: 'Mindestens 8 Zeichen' },
validate: {
hasLetter: (value) => /[A-Za-z]/.test(value) || 'Muss Buchstaben enthalten',
hasNumber: (value) => /\d/.test(value) || 'Muss Zahlen enthalten'
}
})}
type="password"
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>
<div>
<input
{...register('confirmPassword', {
required: 'Passwort bestätigen',
validate: (value) => value === password || 'Passwörter stimmen nicht überein'
})}
type="password"
placeholder="Confirm Password"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
</div>
<button type="submit">Register</button>
</form>
);
}
npm install @hookform/resolvers zodimport { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Schema definieren
const schema = z.object({
email: z.string().email('Ungültige Email-Adresse'),
password: z.string()
.min(8, 'Mindestens 8 Zeichen')
.regex(/[A-Za-z]/, 'Muss Buchstaben enthalten')
.regex(/\d/, 'Muss Zahlen enthalten'),
age: z.number()
.min(18, 'Mindestalter 18')
.max(120, 'Maximalalter 120'),
website: z.string().url('Ungültige URL').optional(),
terms: z.boolean().refine(val => val === true, 'Muss akzeptiert werden')
});
type FormData = z.infer<typeof schema>;
function ZodValidatedForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema)
});
const onSubmit = (data: FormData) => {
console.log(data); // Typesafe und validated
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('email')} type="email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input {...register('password')} type="password" />
{errors.password && <span>{errors.password.message}</span>}
</div>
<div>
<input {...register('age', { valueAsNumber: true })} type="number" />
{errors.age && <span>{errors.age.message}</span>}
</div>
<div>
<input {...register('website')} type="url" />
{errors.website && <span>{errors.website.message}</span>}
</div>
<label>
<input {...register('terms')} type="checkbox" />
Ich akzeptiere die AGB
</label>
{errors.terms && <span>{errors.terms.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Fehler 1: value ohne onChange (Read-Only Input)
// ❌ Falsch: Input ist read-only
<input type="text" value={state} />
// ✓ Richtig: onChange hinzufügen
<input type="text" value={state} onChange={(e) => setState(e.target.value)} />
// ✓ Oder: Uncontrolled mit defaultValue
<input type="text" defaultValue="initial" ref={ref} />
Fehler 2: setState in onChange ohne Event-Extraktion
// ❌ Falsch: Event ist null in async setState
<input onChange={(e) => {
setTimeout(() => {
setState(e.target.value); // Error: e.target is null
}, 100);
}} />
// ✓ Richtig: Wert vorher extrahieren
<input onChange={(e) => {
const value = e.target.value;
setTimeout(() => {
setState(value); // OK
}, 100);
}} />
Fehler 3: Checkbox mit value statt checked
// ❌ Falsch: Checkbox braucht checked
<input
type="checkbox"
value={isChecked}
onChange={(e) => setIsChecked(e.target.value)}
/>
// ✓ Richtig: checked + e.target.checked
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
Fehler 4: Key-Prop bei Listen-Inputs
// ❌ Falsch: Index als key bei dynamischer Liste
{items.map((item, index) => (
<input key={index} value={item.value} />
))}
// ✓ Richtig: Stabiler identifier als key
{items.map((item) => (
<input key={item.id} value={item.value} />
))}
Fehler 5: Validation nur client-seitig
// ❌ Falsch: Nur Frontend-Validation
const handleSubmit = (data) => {
if (validate(data)) {
fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });
}
};
// ✓ Richtig: Server validiert nochmal
const handleSubmit = async (data) => {
if (!validate(data)) return;
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) {
const errors = await response.json();
setServerErrors(errors); // Server-Errors anzeigen
}
} catch (error) {
// Error handling
}
};
Bei Formularen mit vielen Feldern wird jeder Tastendruck zum Performance-Problem – jedes onChange triggert ein Re-Render der gesamten Form.
Problem-Demo:
// ❌ Re-Render bei jedem Tastendruck in allen Feldern
function LargeForm() {
const [formData, setFormData] = useState({
field1: '', field2: '', /* ... */ field50: ''
});
console.log('Form re-rendered'); // Fired bei jedem Tastendruck!
return (
<>
{Object.keys(formData).map(key => (
<input
key={key}
value={formData[key]}
onChange={(e) => setFormData({ ...formData, [key]: e.target.value })}
/>
))}
</>
);
}
Lösung 1: Uncontrolled Components (wenn möglich)
function LargeForm() {
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(formRef.current!);
console.log(Object.fromEntries(formData));
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{Array.from({ length: 50 }, (_, i) => (
<input key={i} name={`field${i}`} defaultValue="" />
))}
<button type="submit">Submit</button>
</form>
);
}
Lösung 2: React Hook Form (Uncontrolled unter der Haube)
import { useForm } from 'react-hook-form';
function LargeForm() {
const { register, handleSubmit } = useForm();
// Keine Re-Renders während Eingabe!
console.log('Form rendered');
return (
<form onSubmit={handleSubmit(console.log)}>
{Array.from({ length: 50 }, (_, i) => (
<input key={i} {...register(`field${i}`)} />
))}
<button type="submit">Submit</button>
</form>
);
}
Formular-Handling in React erfordert bewusste Entscheidungen: Controlled für dynamische UIs mit Live-Feedback, Uncontrolled für Performance, React Hook Form für komplexe Business-Forms. Die Wahl hängt vom Use Case ab – nicht von Dogma.