Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Zur Übersicht
Einstieg 12 Min Lesezeit

useState

Etwas merken, das sich ändern darf

In einem Satz

useState gibt deiner Komponente ein Gedächtnis: einen Wert, der die Durchläufe überlebt, plus einen Knopf, mit dem du ihn änderst und React zum Neuzeichnen bringst.

Das ist der Hook, den du am häufigsten benutzen wirst. Alles, was sich auf dem Bildschirm ändern soll, steckt in einem useState.

signatur.ts
const [wert, setWert] = useState(startwert);
//     ▲       ▲                ▲
//     |       |                zählt nur beim allerersten Durchlauf
//     |       ändert den Wert und lässt React neu zeichnen
//     der Wert dieses Durchlaufs
useState gibt ein Array mit genau zwei Dingen zurück; die eckigen Klammern links packen es aus. Die Namen wählst du selbst.
Bildlich gesprochen

Eine normale Variable in einer Komponente ist ein Zettel, der nach jedem Durchlauf weggeworfen wird. useState ist das Notizbuch, das React für dich aufbewahrt – und sobald du hineinschreibst, malt React das Bild neu.

Wofür du es brauchst

Nimm es, wenn …
  • +

    Text in einem Eingabefeld

  • +

    Auf/Zu, an/aus, ausgewählter Tab

  • +

    Zähler, Auswahl, geladene Daten

  • +

    Kurz: alles, was sich ändert und dabei sichtbar ist

Lass es, wenn …
  • Werte, die du aus anderem State ausrechnen kannst – rechne sie einfach aus

  • Dinge, die niemand sieht (z. B. eine Timer-Nummer) → dafür gibt es RefEin kleiner Merkzettel, den React zwischen den Renders aufhebt – ohne dass eine Änderung daran etwas neu zeichnet.

  • Eine Prop einfach nur kopieren – die Kopie veraltet früher oder später

Zwei Arten zu setzen

setzen.tsx
// 1) Direkt – wenn der neue Wert unabhängig vom alten ist
setName("Ada");
setOffen(false);

// 2) Als Funktion – wenn der neue Wert auf dem alten aufbaut
setCount((c) => c + 1);
setItems((bisher) => [...bisher, neu]);
Tipp Die Faustregel

Taucht der alte Wert rechts auf (count + 1, [...items, x]), nimm die Funktionsform. Sie bekommt garantiert den aktuellsten Stand – auch wenn du mehrfach hintereinander setzt oder der Aufruf verzögert passiert.

Nie das Alte ändern – immer ersetzen

Objekte und Listen aktualisieren
passiert nichts
user.name = "Ada";
setUser(user);

todos.push(neu);
setTodos(todos);
so geht's
setUser({ ...user, name: "Ada" });

setTodos((bisher) => [...bisher, neu]);

React schaut nur, ob es dasselbe Objekt ist wie vorher. Änderst du es innen, ist es immer noch dasselbe – und React zeichnet nicht neu. Merksatz: kopieren, ändern, setzen.

muster.ts
// hinzufügen
setItems((p) => [...p, neu]);

// entfernen
setItems((p) => p.filter((i) => i.id !== id));

// eins davon ändern
setItems((p) => p.map((i) => (i.id === id ? { ...i, done: true } : i)));

// ein Feld in einem Objekt ändern
setForm((f) => ({ ...f, email: "a@b.de" }));
Diese vier Zeilen brauchst du ständig. Es lohnt sich, sie einmal auswendig zu können.
Stolperstein Beim Objekt den Rest nicht vergessen

setForm({ email: v }) ersetzt das ganze Objekt – der Name ist danach weg. Anders als früher in Klassen führt React hier nichts automatisch zusammen. Deshalb immer erst ...f hinschreiben.

Nicht speichern, was sich ausrechnen lässt

zwei Wahrheiten, die auseinanderlaufen
const [items, setItems] = useState([]);
const [anzahl, setAnzahl] = useState(0);

function add(x) {
  setItems([...items, x]);
  setAnzahl(anzahl + 1);   // wehe, das vergisst jemand beim Löschen
}
eine Wahrheit
const [items, setItems] = useState([]);

const anzahl = items.length;   // immer korrekt

function add(x) {
  setItems((p) => [...p, x]);
}

Frag dich bei jedem neuen State: Kann ich das aus etwas anderem ausrechnen? Wenn ja, ist es kein State.

Genauer erklärt: teurer Startwert, Funktionen im State, Batching optional

Startwert nur einmal berechnen

Der Startwert wird ab dem zweiten Durchlauf ignoriert – die Berechnung läuft aber trotzdem jedes Mal, wenn du sie direkt hinschreibst. Übergibst du stattdessen eine Funktion, ruft React sie nur beim ersten Mal auf.

lazy.tsx
// ❌ liest bei JEDEM Durchlauf aus dem Speicher
const [form, setForm] = useState(JSON.parse(localStorage.getItem("f") ?? "{}"));

// ✅ liest nur beim ersten Mal
const [form, setForm] = useState(() => JSON.parse(localStorage.getItem("f") ?? "{}"));

Wenn du wirklich eine Funktion speichern willst

setState(meineFunktion) versteht React als „hier ist die Funktionsform“ und ruft sie auf. Willst du die Funktion selbst ablegen, brauchst du eine Ebene mehr: setState(() => meineFunktion).

Warum mehrere setState nur einmal zeichnen

React sammelt alle Änderungen eines Ereignisses und zeichnet danach einmal neu – das nennt sich BatchingReact sammelt mehrere Änderungen und zeichnet danach einmal neu, statt nach jeder einzelnen.. Seit React 18 gilt das auch in setTimeout und in Promises. Direkt nach setCount(5) steht in count deshalb noch der alte Wert.

batching.tsx
function handleClick() {
  setA(1);
  setB(2);
  setC(3);
  // React zeichnet EINMAL neu, nicht dreimal
}

// Brauchst du den neuen Wert sofort, rechne ihn dir separat aus:
function handleClick2() {
  const naechster = count + 1;
  setCount(naechster);
  sendeAnServer(naechster);   // nicht count benutzen – der ist noch alt
}

Sitzt das schon?

5 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.

Jetzt selbst schreiben

Zähler mit Doppelschritt

Baue den Zähler fertig. Der Knopf +2 ist der interessante: Er soll zweimal setCount aufrufen und wirklich um 2 erhöhen – der naive Weg schafft nur 1.

  • +1 erhöht die Anzeige um 1
  • +2 (zweimal +1) erhöht um 2 – mit zwei setCount-Aufrufen
  • Zurücksetzen setzt auf 0
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  function plusEins() {
    // TODO
  }

  function plusZwei() {
    // TODO: zweimal setCount aufrufen – Ergebnis muss +2 sein
  }

  function reset() {
    // TODO
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <p data-testid="count" style={{ fontSize: 32 }}>{count}</p>
      <button onClick={plusEins}>+1</button>{" "}
      <button onClick={plusZwei}>+2 (zweimal +1)</button>{" "}
      <button onClick={reset}>Zurücksetzen</button>
    </div>
  );
}

Todo-Liste

Vervollständige die drei Funktionen. Wichtig: das vorhandene Array nie verändern, sondern jeweils ein neues bauen.

  • Hinzufügen hängt den Text an und leert das Eingabefeld
  • Ein Klick auf einen Eintrag streicht ihn durch
  • × entfernt den Eintrag
import { useState } from "react";

type Todo = { id: number; text: string; done: boolean };

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");

  function add() {
    if (!text.trim()) return;
    // TODO: neues Todo anhängen und das Eingabefeld leeren
  }

  function toggle(id: number) {
    // TODO: done umschalten
  }

  function remove(id: number) {
    // TODO: Eintrag entfernen
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <input
        aria-label="todo"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Was ist zu tun?"
      />{" "}
      <button onClick={add}>Hinzufügen</button>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <span
              onClick={() => toggle(t.id)}
              style={{ textDecoration: t.done ? "line-through" : "none", cursor: "pointer" }}
            >
              {t.text}
            </span>{" "}
            <button aria-label={"loeschen-" + t.text} onClick={() => remove(t.id)}>×</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Ihr braucht React-Verstärkung im Team?

Wir bauen seit Jahren React- und Next.js-Anwendungen für Kunden in ganz Deutschland – als einzelner Experte, als Verstärkung fürs Bestandsteam oder als komplettes Scrum-Team.

Projekt besprechen →