In der React-Dokumentation bedeutet controlled:
Der Wert eines Elements wird vollständig durch den React-State kontrolliert.
Das Gegenstück ist uncontrolled:
Der Wert liegt im DOM (nicht in React-State) und wird über
refgelesen.
Beispiel – Controlled Input:
<input value={name} onChange={e => setName(e.target.value)} />✅ Controlled: React-State steuert den Wert
Beispiel – Uncontrolled Input:
<input defaultValue="Tom" ref={inputRef} />❌ Uncontrolled: DOM steuert den Wert, React beobachtet nur
Bei komplexeren UI-Komponenten (z. B. Tabs, Accordions, Modals, Dropdowns) geht es um die Frage, wer den React-State kontrolliert.
| Typ | Wer kontrolliert den Zustand? | Beispiel |
|---|---|---|
| Controlled (by parent) | Der Parent hält den State und übergibt ihn über Props | <Tabs currentTab={tab} onTabChange={setTab} /> |
| Self-managed (intern) | Die Komponente verwaltet ihren Zustand intern über React-State | <Tabs defaultTab={0} /> |
Wichtig: Eine self-managed Komponente ist nicht uncontrolled. Sie ist vollständig von React kontrolliert – nur eben innerhalb ihrer eigenen Komponente.
function Tabs({ items }) {
const [tab, setTab] = useState(0);
return (
<ul>
{items.map((item, i) => (
<li key={i} onClick={() => setTab(i)}>
{i === tab ? <b>{item}</b> : item}
</li>
))}
</ul>
);
}function App() {
const [tab, setTab] = useState(0);
return <Tabs items={['A', 'B', 'C']} currentTab={tab} onTabChange={setTab} />;
}Beide Varianten verwenden React-State:
- Im ersten Fall liegt er in der Komponente selbst → self-managed.
- Im zweiten Fall liegt er im Parent → controlled by parent.
| Begriff | Wer kontrolliert den Wert? | Beispiel | React-State involviert? |
|---|---|---|---|
| Controlled Input | React-State | <input value={name} onChange={...} /> |
✅ |
| Uncontrolled Input | DOM (via ref) |
<input defaultValue="Tom" ref={ref} /> |
❌ |
| Self-managed Component | Komponente selbst (React-State intern) | <Tabs defaultTab={0} /> |
✅ |
| Controlled Component (by parent) | Parent-Komponente | <Tabs currentTab={tab} onTabChange={setTab} /> |
✅ |
💡 Kernaussage:
Self-managed Komponenten sind nicht uncontrolled.
Sie sind innerhalb von React kontrolliert (verwenden React-State), werden aber nicht vom Parent gesteuert.
Nur uncontrolled Inputs (z. B. <input defaultValue="..." ref={...} />) sind wirklich außerhalb der Kontrolle von React.
- Controlled Input (Formular): wenn du Live-Validierung, Maskierung, oder synchronen UI-State brauchst.
- Uncontrolled Input: für simple Forms, Performance (große Formulare), oder wenn du den Wert erst beim Submit brauchst.
- Self-managed Widget (Tabs/Accordion/Modal): wenn das Widget eigenständig funktionieren soll.
- Controlled by parent (Widget): wenn der Parent den Zustand zentral steuern/teilen/synchronisieren muss.
A) Controlled Input (Docs-konform)
function NameField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}B) Uncontrolled Input (einfach & performant)
function EmailField() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = () => alert(ref.current?.value);
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
<input defaultValue="tom@example.com" ref={ref} />
<button>Senden</button>
</form>
);
}C) Self-managed Tabs (intern, mit Clamping)
function Tabs({ items, defaultTab = 0 }: { items: string[]; defaultTab?: number }) {
const [tab, setTab] = useState(defaultTab);
useEffect(() => {
// State gültig halten, nicht Props spiegeln
setTab((t) => Math.min(t, Math.max(items.length - 1, 0)));
}, [items]);
return (
<ul role="tablist" aria-orientation="horizontal">
{items.map((label, i) => (
<li key={label}>
<button role="tab" aria-selected={i === tab} onClick={() => setTab(i)}>
{label}
</button>
</li>
))}
</ul>
);
}D) Controlled Tabs (Parent steuert)
function App() {
const [tab, setTab] = useState(0);
return <Tabs items={["A","B","C"]} currentTab={tab} onTabChange={setTab} />;
}E) Explizites Reset-Signal (statt Props→State-Mirroring)
// Parent
<Tabs items={items} defaultTab={0} resetKey={filterVersion} />
// Child
function Tabs({ items, defaultTab = 0, resetKey }: Props) {
const [tab, setTab] = useState(defaultTab);
useEffect(() => { setTab(defaultTab); }, [resetKey, defaultTab]);
// ...
}F) Layout-Effekte ohne setTimeout
const check = useCallback(() => { /* misst/aktualisiert Scroll-Schatten */ }, []);
const scrollTo = useCallback((i: number) => {
const el = listRef.current?.children[i] as HTMLElement | undefined;
if (!el) return;
requestAnimationFrame(() => {
el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
requestAnimationFrame(check);
});
}, [check]);G) ARIA & Tastatur-Navigation für Tabs
<ul role="tablist" aria-orientation="horizontal">
{/* buttons mit role="tab" und aria-selected */}
</ul>
// Keydown: Left/Right/Home/End auf den fokussierten Tab-Button- Props → State spiegeln (nach Mount)
// ❌ Anti-Pattern
const [tab, setTab] = useState(currentTab);
useEffect(() => setTab(currentTab), [currentTab]);
// ✅ Alternative
// a) Controlled by parent: nutze currentTab direkt
// b) Self-managed: nur defaultTab beim Mount verwenden- „Halb controlled, halb self-managed“ (zwei Quellen der Wahrheit)
// ❌ onClick setzt internen State, Effekt setzt aus Props zurück → flakeyFix: Entweder controlled oder self-managed. Hybride nur mit explizitem Reset-Signal (resetKey).
setStateim Render
// ❌ im Body der Komponente abhängig von Props setState aufrufen → Render-Loop-RisikoFix: Ableitungen direkt berechnen oder Effekte gezielt einsetzen.
setTimeoutfür Layout/Scroll verwenden
// ❌ fragil, timing-abhängigFix: requestAnimationFrame/ResizeObserver nutzen; danach messen/aktualisieren.
valueunddefaultValuemischen
// ❌ führt zu kontrolliert↔unkontrolliert-WechselwarnungenFix: Entweder value oder defaultValue – nicht beides.
- Kein Clamping bei Listendaten
// ❌ Index zeigt ins Leere, wenn items schrumpfenFix: Beim items-Wechsel Index auf [0, items.length-1] begrenzen.
- Fehlende ARIA/Tastaturunterstützung
- Tabs ohne
role="tablist",role="tab",aria-selected - Keine Pfeiltasten-/Home/End-Unterstützung → schlechtere Accessibility
- Brauche ich zentralen Zustand über mehrere Komponenten? → Controlled by parent
- Soll das Widget autark funktionieren? → Self-managed
- Muss ein Input live validiert/formatiert werden? → Controlled Input
- Ist es ein großes Formular mit seltenem Lesen der Werte? → Uncontrolled Inputs
- Gibt es potenzielles Out-of-bounds (Filter, Sort, dynamische Items)? → Clamping einplanen
- Gibt es Layout-Effekte (Scroll/Schatten/Resize)? → rAF/ResizeObserver statt Timeouts
Keine zwei Wahrheiten. Entscheide dich pro Komponente für eine Quelle der Wahrheit.
Inputs: controlled (React) vs. uncontrolled (DOM).
Widgets: controlled by parent vs. self-managed.
Vermeide Props→State-Sync nach dem Mount, nutzedefault*,resetKey, Clamping, rAF und ARIA – dann ist die UI stabil, zugänglich und gut testbar.