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

Stolpersteine: State & JSX

Die Fehler, die jede:r einmal macht – meist mehrmals

In einem Satz

Sieben Fallen rund um State und JSX, jeweils mit dem Denkfehler dahinter – damit du sie im eigenen Code wiedererkennst.

Diese sieben Fehler macht praktisch jede:r – meistens mehrmals. Wenn du sie einmal gesehen hast, erkennst du sie später in Sekunden statt in Stunden.

1. Das Vorhandene ändern statt zu ersetzen

So nicht
todos[0].done = true;
setTodos(todos);

user.adresse.stadt = "Bern";
setUser(user);
So geht's
setTodos((p) =>
  p.map((t, i) => (i === 0 ? { ...t, done: true } : t))
);

setUser((u) => ({
  ...u,
  adresse: { ...u.adresse, stadt: "Bern" },
}));

Warum passiert das? In normalem JavaScript ändert man Objekte einfach (MutationEin vorhandenes Objekt oder Array direkt verändern, statt ein neues zu erzeugen – zum Beispiel mit push().) – das ist Gewohnheit. React schaut aber nur, ob es noch dasselbe Objekt ist (ReferenzBei Objekten und Arrays speichert JavaScript nicht den Inhalt, sondern die Adresse. Zwei Adressen sind verschieden, auch wenn der Inhalt gleich aussieht.Deshalb ist { a: 1 } === { a: 1 } in JavaScript false.). Bei tief verschachtelten Daten wird das Kopieren mühsam; dann lohnt sich Immer oder eine flachere Struktur.

2. Den eingefrorenen Wert weiterrechnen

stale.tsx
// Nach dem Klick steht dort 1, nicht 3:
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// Und der Klassiker im Timer – zählt genau einmal:
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);            // count ist hier für immer 0
Stolperstein Der Timer merkt sich den alten Wert

Die Funktion im Interval wurde einmal erzeugt – und hält seitdem den count von damals fest (Fachbegriff: Stale ClosureEine Funktion arbeitet mit veralteten Werten, weil sie in einem früheren Durchlauf entstanden ist und die alten Werte festhält.). Lösung: setCount((c) => c + 1). Dann brauchst du den Wert gar nicht mehr von außen.

3. Props in den State kopieren

läuft auseinander
function Preis({ betrag }) {
  const [wert, setWert] = useState(betrag);
  // ändert sich betrag später, bleibt wert stehen
  return <span>{wert}</span>;
}
Prop direkt benutzen
function Preis({ betrag }) {
  return <span>{betrag}</span>;
}

Der Startwert wird nur beim ersten Render benutzt. Brauchst du wirklich einen bearbeitbaren Entwurf (Formularfeld mit Vorbelegung), dann benenne es auch so: const [entwurf, setEntwurf] = useState(betrag) – und setze es per key zurück, wenn ein anderer Datensatz kommt.

4. Abgeleitetes doppelt speichern

So nicht
const [preise, setPreise] = useState([]);
const [summe, setSumme] = useState(0);
const [leer, setLeer] = useState(true);
So geht's
const [preise, setPreise] = useState([]);

const summe = preise.reduce((a, b) => a + b, 0);
const leer = preise.length === 0;

Jede zusätzliche State-Variable ist eine Stelle, an der man das Aktualisieren vergessen kann. Frage: Kann ich das ausrechnen? Dann ist es kein State.

5. Der Index als key

keys.tsx
// Problematisch, sobald sortiert, gefiltert, eingefügt oder gelöscht wird:
{items.map((item, i) => <Zeile key={i} item={item} />)}

// Richtig: eine Identität aus den Daten
{items.map((item) => <Zeile key={item.id} item={item} />)}

// Keine ID vorhanden? Beim Anlegen eine vergeben:
const neu = { id: crypto.randomUUID(), text };
Stolperstein Woran man es merkt

Checkboxen sind nach dem Löschen bei den falschen Zeilen angehakt, Eingabefelder behalten fremden Text, Animationen laufen am falschen Element. Alles derselbe Fehler.

6. `&&` mit Zahlen

rendert eine 0 auf der Seite
{items.length && <Liste items={items} />}
{anzahl && <Badge n={anzahl} />}
explizit als Bedingung
{items.length > 0 && <Liste items={items} />}
{anzahl ? <Badge n={anzahl} /> : null}

0 && x ergibt 0 – und React rendert die Zahl 0 brav auf den Bildschirm. Bei false, null und undefined passiert nichts, bei 0 schon.

7. State zu weit oben (oder zu weit unten)

State gehört zum nächsten gemeinsamen Elternteil aller Komponenten, die ihn brauchen – nicht höher. Liegt der Suchtext in App, rendert bei jedem Tastendruck die ganze Anwendung neu. Liegt er zu tief, kommen die Geschwister nicht daran.

Tipp Faustregel

So tief wie möglich, so hoch wie nötig. Wandert State nach oben, überlege zuerst, ob nicht auch die Komponente mit nach unten wandern kann.

Sitzt das schon?

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

Jetzt selbst schreiben

Drei Bugs auf einmal

In dieser Komponente stecken drei typische Fehler: ein doppelt gehaltener abgeleiteter Wert (der beim Löschen vergessen wird), das &&-Problem mit der 0 und ein Index als key. Räum sie auf.

  • Bei leerer Liste erscheint keine 0 auf der Seite
  • Nach Hinzufügen und Löschen stimmt die Anzahl weiterhin
  • Der zweite State verschwindet – die Anzahl wird berechnet
  • Die Einträge nutzen ihre id als key statt des Index
import { useState } from "react";

type Item = { id: number; text: string };

let naechsteId = 1;

export default function App() {
  const [items, setItems] = useState<Item[]>([]);
  const [anzahl, setAnzahl] = useState(0);   // Bug 1: abgeleitet, aber doppelt gehalten

  function add() {
    const id = naechsteId++;
    setItems((prev) => [...prev, { id, text: "Eintrag " + id }]);
    setAnzahl(anzahl + 1);
  }

  function remove(id: number) {
    setItems((prev) => prev.filter((i) => i.id !== id));
    // hier wurde das Mitzählen vergessen – und genau das ist der Punkt
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <button onClick={add}>Hinzufügen</button>
      <p data-testid="anzahl">Anzahl: {anzahl}</p>

      {/* Bug 2: rendert eine 0, wenn die Liste leer ist */}
      {items.length && (
        <ul>
          {/* Bug 3: der Index als key */}
          {items.map((item, i) => (
            <li key={i}>
              {item.text}{" "}
              <button aria-label={"weg-" + item.id} onClick={() => remove(item.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 →