# React Controllable State Hook: Code und 3 Fallstricke

> Bau einen React Controllable State Hook, der value oder defaultValue annimmt. Mit komplettem TypeScript-Code, drei echten Fallstricken und dem Radix-Vergleich.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind)
- Lesezeit: 11 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-controllable-state-hook
- Englische Fassung: https://wingo-ui.com/blog/react-controllable-state-hook

## Kurz gesagt

Ein React Controllable State Hook gibt wie useState ein Paar [value, setValue] zurück, folgt aber dem value-Prop, sobald die Elternkomponente einen übergibt, und hält sonst einen eigenen State, der mit defaultValue startet. Die Komponente ruft bei jeder Interaktion den Setter auf; der Hook speichert den Wert nur im uncontrolled Modus und ruft onChange in beiden Modi auf. Lass jede Instanz ihr ganzes Leben lang in einem Modus: Wer einen Wert mit undefined leert, gibt die Kontrolle stillschweigend an einen veralteten internen State zurück.

Jedes wiederverwendbare Eingabefeld bekommt irgendwann dieselbe Anfrage. Ein Screen will den Wert selbst verwalten (`value` plus `onChange`), ein anderer braucht nur einen Startwert (`defaultValue`) und will nur erfahren, wenn er sich ändert. Mit einem React Controllable State Hook unterstützt du beides in einer Komponente, ohne zwei Codepfade: Du schreibst die Komponente gegen ein einziges Paar `[value, setValue]`, und der Hook entscheidet, wem der State gehört. Dieses Tutorial baut den Hook, zeigt die Bugs, die entstehen, wenn ein Prop den Modus wechselt, und erklärt, wie [Switch](https://wingo-ui.com/components/switch) und [Tabs](https://wingo-ui.com/components/tabs) dasselbe Muster nutzen.

## Was ist der Unterschied zwischen controlled und uncontrolled Komponenten?

Der Unterschied liegt darin, wem der Wert gehört: der Elternkomponente (controlled) oder der Komponente selbst (uncontrolled). In deutschen Texten heißen sie auch kontrollierte und unkontrollierte Komponenten. Die [React-Doku zum Teilen von State](https://react.dev/learn/sharing-state-between-components) beschreibt es so: Eine Komponente ist uncontrolled, wenn ihr wichtiger State in ihr selbst liegt, und controlled, wenn Props der Elternkomponente diesen State bestimmen.

Das native `<input>` zeigt beide Formen:

```tsx
// uncontrolled: das Feld hält den Text, die Elternkomponente legt nur den Startwert fest
<input defaultValue="Emma Carter" />

// controlled: die Elternkomponente hält den Text und muss ihn bei jedem Tastendruck aktualisieren
<input value={name} onChange={(event) => setName(event.target.value)} />
```

Das ist der Zielkonflikt bei jeder Entscheidung „React controlled vs uncontrolled“. Uncontrolled bedeutet weniger Code für den Aufrufer. Controlled brauchst du, wenn die Elternkomponente validieren, zurücksetzen, mit der URL synchronisieren oder mehrere Controls von einer Stelle aus steuern muss. Eine Komponente aus einer Bibliothek sollte keine der beiden Varianten erzwingen. Deshalb nimmt sie controlled und uncontrolled Props nebeneinander an: `value`, `defaultValue` und `onValueChange`. Die Namen richten sich nach dem Wert: `checked` / `defaultChecked` / `onCheckedChange` bei einem Switch, `open` / `defaultOpen` / `onOpenChange` bei einem Dialog.

## Was macht ein React Controllable State Hook?

Er gibt der Komponente einen Wert und einen Setter und versteckt die Frage, wem der State gehört:

- Solange der `value`-Prop definiert ist, gibt der Hook ihn zurück und speichert nie eine eigene Kopie. Der Setter ruft nur `onChange` auf, und die Elternkomponente entscheidet, ob sie die Änderung übernimmt.
- Solange `value` undefined ist, hält der Hook den State selbst, beginnend mit `defaultValue`, und der Setter aktualisiert ihn und ruft `onChange` auf.

Hier ist der komplette Hook, wie er in Wingo UI ausgeliefert wird (`hooks/use-controllable-state.ts`):

```ts
import { useCallback, useInsertionEffect, useRef, useState, type SetStateAction } from "react";

export type UseControllableStateParams<T> = {
  value?: T;
  defaultValue: T;
  onChange?: (value: T) => void;
};

export function useControllableState<T>({
  value,
  defaultValue,
  onChange,
}: UseControllableStateParams<T>): [T, (next: SetStateAction<T>) => void] {
  const [uncontrolled, setUncontrolled] = useState(defaultValue);
  const controlled = value !== undefined;
  const current = controlled ? value : uncontrolled;

  // die Werte des letzten Renders, gelesen vom stabilen Setter
  const latest = useRef({ current, controlled, onChange });
  useInsertionEffect(() => {
    latest.current = { current, controlled, onChange };
  });

  const setValue = useCallback((next: SetStateAction<T>) => {
    const { current: prev, controlled: isControlled, onChange: notify } = latest.current;
    const resolved = typeof next === "function" ? (next as (prev: T) => T)(prev) : next;
    if (Object.is(resolved, prev)) return;
    if (!isControlled) {
      latest.current = { ...latest.current, current: resolved };
      setUncontrolled(resolved);
    }
    notify?.(resolved);
  }, []);

  return [current, setValue];
}
```

Auf drei Details kommt es an. Der Setter ist stabil (`useCallback` ohne Dependencies), also kannst du ihn bedenkenlos in Dependency-Arrays von Effects und in memoisierten Kindkomponenten verwenden. Er liest den aktuellen Wert und den Callback aus einer Ref, sodass eine Elternkomponente, die bei jedem Render ein neues Inline-`onChange` übergibt, nie einen veralteten bekommt. Und er nimmt wie `useState` eine Updater-Funktion an, also funktioniert `setValue((prev) => !prev)` in beiden Modi.

Die Ref wird in `useInsertionEffect` geschrieben, nicht während des Renders. `latest.current = ...` direkt im Render-Body sieht gleichwertig aus. Ein Render, den React verwirft (ein suspendierter oder unterbrochener Concurrent Render), würde die Ref aber auf Werten stehen lassen, die nie committet wurden, und der Lint des React Compilers markiert Ref-Schreibzugriffe während des Renders. Der Insertion Effect läuft vor den Layout Effects. Selbst ein `useLayoutEffect` in einer Kindkomponente, der den Setter aufruft, sieht also die committeten Werte.

Installier ihn einzeln mit:

```bash
npx wingo-ui@latest add use-controllable-state
```

Die [Doku zu useControllableState](https://wingo-ui.com/components/use-controllable-state) listet die Parameter und den Rückgabewert.

## Wie schreibe ich eine Komponente, die value, defaultValue und onChange unterstützt?

Deklarier die drei Props, gib sie an den Hook weiter und schreib den Rest der Komponente so, als gehöre ihr der State. Ein Toggle-Button:

```tsx
"use client";

import { useControllableState } from "@/hooks/use-controllable-state";

type ToggleProps = {
  pressed?: boolean;
  defaultPressed?: boolean;
  onPressedChange?: (pressed: boolean) => void;
};

export function Toggle({ pressed, defaultPressed = false, onPressedChange }: ToggleProps) {
  const [on, setOn] = useControllableState({
    value: pressed,
    defaultValue: defaultPressed,
    onChange: onPressedChange,
  });

  return (
    <button type="button" aria-pressed={on} onClick={() => setOn((prev) => !prev)}>
      {on ? "An" : "Aus"}
    </button>
  );
}
```

Jetzt funktionieren beide Aufrufe mit derselben Komponente:

```tsx
// uncontrolled: startet eingeschaltet und hält seinen State selbst
<Toggle defaultPressed onPressedChange={(on) => console.log(on)} />

// controlled: der State gehört der Elternkomponente, und sie kann eine Änderung ablehnen
<Toggle pressed={muted} onPressedChange={(next) => setMuted(next)} />
```

Die Demo unten zeigt denselben Mengen-Stepper zweimal. Drück auf beiden Karten Plus und Minus: Der linke zählt selbst, der rechte wird von einer Elternkomponente gesteuert, die ihn bei 5 deckelt und zurücksetzen kann.

> Live-Demo (useControllableState): Drück auf beiden Steppern Plus: Der uncontrolled zählt bis 9, der controlled bleibt bei 5 stehen, weil seine Elternkomponente höhere Werte ablehnt. Probier sie unter [useControllableState](https://wingo-ui.com/components/use-controllable-state) aus und installier sie mit `npx wingo-ui@latest add use-controllable-state`.

Beachte, was die Obergrenze auf der controlled Seite kostet: nichts im Stepper. Die Elternkomponente bekommt `onChange(6)`, speichert `5`, und der Stepper rendert den `value`, den er bekommt.

## Was geht schief, wenn eine Komponente zwischen controlled und uncontrolled wechselt?

Die Komponente wechselt den Besitzer, ohne es jemandem zu sagen, und zeigt einen Wert, mit dem niemand rechnet. Der Hook entscheidet bei jedem Render mit `value !== undefined` über den Modus, also kann der Aufrufer ihn versehentlich umschalten. Die [React-Referenz zu input](https://react.dev/reference/react-dom/components/input) nennt die Regel für native Inputs: Ein Input soll während seiner gesamten Lebensdauer nicht zwischen controlled und uncontrolled wechseln, und React loggt eine Warnung, wenn es das doch tut. Zwei der folgenden Fallstricke kommen von diesem Wechsel, der dritte davon, dass `defaultValue` wie ein Wert behandelt wird.

### Ein mit undefined geleerter Wert zeigt veralteten internen State

Der klassische Fall ist eine Tab-Leiste, die an optionale Daten gebunden ist:

```tsx
// falsch: hat der Nutzer keinen Tarif, werden die Tabs uncontrolled
<Tabs options={plans} value={user.plan} onValueChange={setPlan} />
```

Solange `user.plan` ein String ist, gehört der Tab der Elternkomponente, und der Hook schreibt nie in seinen internen State. Dieser State enthält aber noch, was er hatte, bevor die Elternkomponente übernommen hat, oft den ursprünglichen `defaultValue` (bei Tabs die erste aktivierte Option). Sobald `user.plan` zu `undefined` wird, gibt der Hook diesen alten Wert zurück, und kein `onChange` informiert die Elternkomponente.

Die Lösung: Halt den Wert definiert. Ersetz ihn durch einen Fallback (`value={user.plan ?? ""}`, das Tabs ohne aktiven Tab rendert), oder nimm in deinen eigenen Komponenten `null` für „nichts ausgewählt“. Der Hook von Wingo UI behandelt nur `undefined` als uncontrolled, also bleibt `null` ein controlled leerer Wert. Radix macht denselben Check mit `prop !== undefined`. Native Inputs sind strenger: React will als `value` einen String, nie `null`, also nimm dort `""` als Fallback.

### Beim Laden startet die Komponente uncontrolled

Das Gegenstück tritt beim ersten Laden auf:

```tsx
// falsch: uncontrolled, bis der Request fertig ist, dann übernimmt der Serverwert
<Tabs options={plans} value={settings.data?.plan} onValueChange={savePlan} />
```

Während der Request läuft, ist `value` undefined. Die Tabs halten den State also selbst, und der Nutzer kann schon herumklicken. Kommen die Daten an, wird die Komponente controlled und springt auf den Serverwert; der Tab, den der Nutzer gewählt hat, ist weg. Hat `savePlan` diese Auswahl schon gespeichert, zeigt der Screen jetzt womöglich einen veralteten Wert aus der ersten Antwort. Render das Control erst, wenn die Daten da sind, oder lass es uncontrolled und mounte es mit `key={settings.data.plan}` und `defaultValue={settings.data.plan}`, sobald die Daten vorliegen.

### Ein neuer defaultValue wird ignoriert

`defaultValue` ist der Startwert von `useState` und wird deshalb einmal gelesen, beim Mount. Eine spätere Änderung bewirkt nichts. Das ist richtig (es ist ein Default, kein Wert) und beim ersten Mal trotzdem überraschend. Soll ein neuer Default die Komponente neu starten, ändere ihren `key`. Die Stepper-Demo oben macht genau das: Das `defaultValue`-Control im Playground ist zugleich der `key` des Steppers. Mit demselben Trick gibt eine Elternkomponente die Kontrolle sauber ab: Sie mountet die Komponente mit einem neuen `key` neu und übergibt den aktuellen Wert als `defaultValue`.

## Wann ruft der Setter onChange auf?

Nur wenn sich der Wert wirklich ändert. Der Setter berechnet den nächsten Wert, vergleicht ihn per `Object.is` mit dem aktuellen und bricht ab, wenn beide gleich sind. Ein Klick auf einen schon aktiven Tab löst also kein `onValueChange` aus, und eine Elternkomponente, die Änderungen loggt, sieht nur echte Änderungen. Objekte werden per Referenz verglichen: Ein neues Objekt mit gleichem Inhalt zählt trotzdem als Änderung.

`Object.is` statt `!==` ist bei Zahlen wichtig. `NaN !== NaN` ist true. Mit einem `!==`-Check würde ein Zahlenfeld, das seinen leeren Text zu `NaN` parst, also jedes Mal `onChange(NaN)` auslösen, wenn es diesen leeren Text erneut parst. `Object.is(NaN, NaN)` ist true, also meldet der Hook nichts. Es ist derselbe Vergleich, mit dem `useState` einen Re-Render überspringt.

Der Aufruf ist synchron, im Setter, in beiden Modi. In einem Event-Handler heißt das: Das `setState` der Elternkomponente in `onChange` wird mit dem Update der Komponente gebatcht, und beide rendern zusammen.

Eine Feinheit solltest du kennen. Im uncontrolled Modus bauen zwei Updater im selben Event aufeinander auf, wie bei `useState`: Zweimal `setValue((n) => n + 1)` addiert 2. Im controlled Modus ändert sich der Wert erst, wenn die Elternkomponente neu rendert. Beide Updater sehen also dasselbe `prev` und melden zweimal dieselbe Zahl. Radix verhält sich genauso, weil auch sein controlled Setter den Updater gegen den aktuellen Prop auflöst. Muss eine Komponente mehrere Schritte auf einmal anwenden, berechne den Endwert und ruf den Setter einmal auf.

## Wie nutzen Switch und Tabs den Hook?

152 der 227 Dateien in `components/ui` importieren `useControllableState` (im Oktober 2026 im Repository gezählt). Die Konvention ist ein Wert-Prop, ein Default-Prop und ein Change-Callback, benannt nach dem Wert. Tabs und Switch zeigen die zwei Wege, das zu verdrahten.

### Tabs: Der Hook verwaltet den Wert

Die einfache `Tabs`-API reicht `value`, `defaultValue` und `onValueChange` direkt an den Hook weiter, mit einer Besonderheit der Bibliothek: Fehlt `defaultValue`, startet Tabs auf der ersten aktivierten Option. Uncontrolled ist das eine Zeile:

```tsx
import { Tabs, type TabsOption } from "@/components/ui/tabs";

const options: TabsOption[] = [
  { value: "active", label: "Aktiv", badge: 12, content: <p>12 Inserate sind in New York online.</p> },
  { value: "pending", label: "Ausstehend", badge: 3, content: <p>3 Inserate warten auf Prüfung.</p> },
  { value: "sold", label: "Verkauft", content: <p>Letzter Verkauf: 2. Sep. 2026.</p> },
];

export function MyListings() {
  return <Tabs options={options} defaultValue="pending" aria-label="Meine Inserate" />;
}
```

Im controlled Modus hält die Elternkomponente den Tab, zum Beispiel um ihn mit etwas anderem auf der Seite synchron zu halten (dieselben `options` wie oben):

```tsx
"use client";

import { useState } from "react";
import { Tabs } from "@/components/ui/tabs";

export function ListingsPanel() {
  const [tab, setTab] = useState("active");
  return <Tabs options={options} value={tab} onValueChange={setTab} aria-label="Meine Inserate" />;
}
```

Weil der Setter über den Hook läuft, kann Tabs den Wert auch selbst ändern und eine controlled Elternkomponente trotzdem informieren: Verschwindet der aktive Tab aus `options`, ruft Tabs den Setter mit dem ersten aktivierten Tab auf, und eine controlled Elternkomponente bekommt das über `onValueChange` mit.

### Switch: Der Hook speichert, die Komponente meldet

[Switch](https://wingo-ui.com/components/switch) nutzt den Hook ohne `onChange`:

```ts
const [checked, setChecked] = useControllableState({ value: checkedProp, defaultValue: defaultChecked });
```

Der Grund ist der Rückgabewert. `onCheckedChange` ist als `(checked: boolean) => void | Promise<unknown>` typisiert, und der Switch braucht dieses Promise: Er schaltet sofort um, zeigt während des Speicherns einen Spinner im Thumb und schaltet zurück, wackelt und zeigt den Fehlertext, wenn das Promise rejected wird. Der Hook ruft `onChange` auf und verwirft, was zurückkommt. Deshalb ruft der Switch `onCheckedChange` selbst auf und nutzt den Hook nur zum Speichern. Das ist eine brauchbare Regel für deine eigenen Komponenten: Wenn das Ergebnis des Callbacks zählt, ruf ihn direkt auf und lass den Hook den Wert halten.

```tsx
"use client";

import { Switch } from "@/components/ui/switch";

async function saveSetting(key: string, on: boolean) {
  const res = await fetch("/api/settings", {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ [key]: on }),
  });
  if (!res.ok) throw new Error("Speichern fehlgeschlagen");
}

export function OffersSetting() {
  return (
    <Switch
      label="Preisangebote"
      defaultChecked
      errorText="Speichern fehlgeschlagen. Versuch es noch mal."
      onCheckedChange={(on) => saveSetting("offers", on)}
    />
  );
}
```

In der Demo unten schaltet der Button „Simulate an error“ den ersten Switch mit einem Speichervorgang um, der fehlschlägt. Schaltest du selbst einen Switch um, bleibt die Änderung bestehen.

> Live-Demo (Switch): Drück auf Simulate an error: Der erste Switch schaltet sofort um, zeigt einen Spinner im Thumb, springt dann zurück und zeigt den Fehler. Probier sie unter [Switch](https://wingo-ui.com/components/switch) aus und installier sie mit `npx wingo-ui@latest add switch`.

In diesem uncontrolled Aufbau hält der Switch den Wert, und die Elternkomponente speichert ihn nie; das Promise entscheidet nur, ob das Umschalten bleibt. Braucht ein Formular den Wert im State der Elternkomponente, übergib stattdessen `checked` und `onCheckedChange` und setz `checked`, wenn das Speichern geklappt hat.

## Wie unterscheidet sich der Hook von Radix useControllableState?

Radix useControllableState hat dieselbe Grundform, unterscheidet sich aber im Detail. Radix Primitives nutzen den Hook intern, und er ist auf npm als `@radix-ui/react-use-controllable-state` veröffentlicht (Version 1.3.0, Stand Oktober 2026). Jede shadcn/ui-Komponente, die auf Radix aufbaut, erbt also sein Verhalten. Verglichen mit dem [Radix-Quellcode auf GitHub](https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx), Stand Oktober 2026:

| | Wingo UI `useControllableState` | Radix `useControllableState` |
| --- | --- | --- |
| Optionen | `value`, `defaultValue`, `onChange` | `prop`, `defaultProp`, `onChange`, `caller` |
| Controlled, wenn | `value !== undefined` | `prop !== undefined` |
| Änderungs-Check | `Object.is` | `!==` |
| Uncontrolled `onChange` | im Setter aufgerufen | aus einem Effect nach dem Render aufgerufen |
| Identität des Setters | stabil für das ganze Leben der Komponente | neu, sobald sich der Prop oder der Modus ändert |
| Warnung beim Moduswechsel | keine | ein `console.warn` im Development, mit `caller` im Text |
| Installation | als Quellcode in dein Projekt kopiert | ein npm-Paket |

Zwei Zeilen ändern, wie du Code drumherum schreibst. Der Setter von Radix führt `prop` in seinen `useCallback`-Dependencies. Im controlled Modus ist er also bei jeder Wertänderung eine neue Funktion, und ein Effect, der von ihm abhängt, läuft bei jeder Änderung erneut. Und weil Radix uncontrolled Änderungen aus einem Effect meldet, rendert eine Elternkomponente, die den Wert in `onChange` speichert, einen Commit später als die Komponente.

Die Warnung ist der einzige Punkt, den wir als Grund nennen würden, Radix vorzuziehen: Sie fängt die oben beschriebenen Moduswechsel schon im Development ab. Unser Hook bleibt eine einzige Datei, die dir gehört, mit Namen, die zu den Props passen, die er bedient.

## Nächste Schritte

Kopier den Hook mit `npx wingo-ui@latest add use-controllable-state` und nutz ihn in deiner nächsten Komponente, die einen Wert-Prop braucht. Switch und Tabs sind ebenfalls kostenlos: Die [Doku zur Tabs-Komponente](https://wingo-ui.com/components/tabs) listet die Props `value`, `defaultValue` und `onValueChange`, und die [Doku zur Switch-Komponente](https://wingo-ui.com/components/switch) erklärt das asynchrone Speichern mit `onCheckedChange`. Wenn du deine Komponenten gleichzeitig auf React 19 umstellst, behandelt [ref als Prop in React 19](https://wingo-ui.com/de/blog/react-19-forwardref-ref-as-prop) die andere Hälfte einer modernen Komponenten-API, und der [Guide zu Design Tokens in Tailwind v4](https://wingo-ui.com/de/blog/tailwind-v4-design-tokens) ist der Einstieg in die übrigen [Artikel zu Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind).

## Komponenten in diesem Artikel

- [useControllableState](https://wingo-ui.com/components/use-controllable-state) (Free): A React hook for controlled and uncontrolled state: it follows a value prop when you pass one and keeps its own state when you do not. Installation: `npx wingo-ui@latest add use-controllable-state`
- [Switch](https://wingo-ui.com/components/switch) (Free): A React toggle switch for settings that apply at once: a spring thumb that stretches and drags, thumb icons, and async saves that roll back. Installation: `npx wingo-ui@latest add switch`
- [Tabs](https://wingo-ui.com/components/tabs) (Free): React tabs with a sliding indicator that switch views of one page: underline for sections, pill for filters, segmented for a compact switch. Installation: `npx wingo-ui@latest add tabs`

## Häufige Fragen

### Was ist der Unterschied zwischen controlled und uncontrolled Komponenten in React?

Eine controlled Komponente zeigt den Wert, den die Elternkomponente übergibt, und meldet Änderungen über einen Callback; der State gehört also der Elternkomponente. Eine uncontrolled Komponente hält ihren State selbst, meist mit einem defaultValue-Prop als Startwert, und die Elternkomponente erfährt nur von Änderungen.

### Was macht useControllableState?

Der Hook gibt wie useState einen Wert und einen Setter zurück. Solange der value-Prop definiert ist, gibt er diesen Prop zurück und der Setter ruft nur onChange auf; ist er undefined, speichert der Hook den Wert selbst, beginnend mit defaultValue.

### Darf eine Komponente von uncontrolled zu controlled wechseln?

Sie sollte es nicht. React warnt, wenn ein Input das tut, Radix warnt im Development, und der Hook von Wingo UI folgt einfach dem, was er bekommt, und zeigt dann einen veralteten Wert, wenn die Kontrolle zurückgeht. Leg dich pro Instanz auf einen Modus fest und nimm null statt undefined für einen leeren controlled Wert.

### Warum aktualisiert sich meine Komponente nicht, wenn sich defaultValue ändert?

defaultValue wird einmal gelesen, beim Mount, genau wie der Startwert von useState. Um mit einem neuen Default neu zu starten, gib der Komponente einen neuen key oder steuere sie über value.

### Wie unterscheidet sich der Hook von Wingo UI von Radix useControllableState?

Radix nennt die Optionen prop, defaultProp, onChange und caller, vergleicht mit !== und ruft onChange bei uncontrolled Änderungen in einem Effect nach dem Render auf. Der Hook von Wingo UI nimmt value, defaultValue und onChange, vergleicht mit Object.is und ruft onChange synchron im Setter auf, ohne Warnung im Development.

### Ist der Hook useControllableState kostenlos?

Ja. Der Hook ist kostenlos, ebenso Switch und Tabs, die auf ihm aufbauen. Installier ihn mit npx wingo-ui@latest add use-controllable-state.

---

Quelle: https://wingo-ui.com/de/blog/react-controllable-state-hook
