32 Formular-Handling – Controlled, Uncontrolled und Validation

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.

32.1 Das Grundproblem: Wer kontrolliert den State?

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.

32.2 Controlled Components: React als Single Source of Truth

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

32.2.1 TypeScript: Event-Typisierung

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>
  );
}

32.2.2 Verschiedene Input-Typen

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

32.3 Uncontrolled Components: DOM als Source of Truth

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

32.3.1 defaultValue vs value: Der kritische Unterschied

// ❌ 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!

32.4 Client-Side Validation: Verschiedene Ansätze

32.4.1 Native HTML5 Validation

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

32.4.2 Custom Validation mit State

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.

32.4.3 Wiederverwendbare Validation Hook

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>
  );
}

32.5 React Hook Form: Die moderne Lösung

Für komplexe Formulare ist React Hook Form der aktuelle Standard. Performant, wenig Boilerplate, TypeScript-Integration.

npm install react-hook-form

32.5.1 Basis-Verwendung

import { 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>
  );
}

32.5.2 Custom Validation Logic

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>
  );
}

32.5.3 Integration mit Zod für Schema-Validation

npm install @hookform/resolvers zod
import { 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>
  );
}

32.6 Häufige Fehler

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
  }
};

32.7 Performance: Optimierung großer Formulare

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.