State Hook im Detail

State Hook im Detail

zusätzliche Funktionalität des State Hooks:

  • Ãœbergeben einer Initialisierungsfunktion an den Hook, damit State nur einmal initialisiert wird
  • Ãœbergeben einer "State-Transformations-Funktion" an den Setter - die beschreibt, wie State aktualisiert werden soll

Initialisierungsfunktion

mögliches Performance-Problem: der initiale State wird bei jedem Rendering erneut generiert - und wird aber nur beim ersten Rendering benötigt

const [foo, setFoo] = useState(
  createObjectWithManyEntries('foo')
);

Initialisierungsfunktion

Lösung: Führe die Initialisierung nur beim ersten Aufruf durch - durch das Übergeben einer Funktion, die nur beim ersten Rendering verwendet wird

const [foo, setFoo] = useState(() =>
  createObjectWithManyEntries('foo')
);

State-Transformations-Funktion

grundlegende Version für das Aktualisieren von State:

setCount(count + 1);

alternative Version, die veralteten State vermeidet:

setCount((c) => c + 1);

(siehe nächster Abschnitt)

Veralteter State

Veralteter State

manchmals haben wir keinen direkten Zugriff auf den aktuellsten State, wenn wir einen neuen State festsetzen

spezielles Szenario: asynchrone Events in Funktionskomponenten (z.B. Netzwerkanfragen)

Veralteter State

Szenarien:

Wird in einem Event-Handler ein State-Setter aufgerufen, so ist der neueste State erst nach dem vollständigen Ausführen des Event-Handlers verfügbar

Wird eine asynchrone Aktion aus einer Funktionskomponente ausgelöst, so kann sie während der Ausführung weiter auf alten State verweisen

Veralteter State

Beispiel 1:

const [count, setCount] = useState(0);

function incrementTwice() {
  setCount(count + 1);
  setCount(count + 1);
}

Der Code setzt count zweimal hintereinander auf 1

Veralteter State

Beispiel 2:

const [count, setCount] = useState(0);

function increment() {
  setCount(count + 1);
}
function incrementWithDelay() {
  setTimeout(() => setCount(count + 1), 3000);
}

Veralteter State

Szenario für Beispiel 2:

  • count startet bei 0
  • incrementWithDelay wird aufgerufen
  • nach 1 Sekunde wird increment aufgerufen
  • nach 2 Sekunden wird increment erneut aufgerufen
  • nach 3 Sekunden aktualisiert incrementWithDelay den Wert

Werte von count: 0 → 1 → 2 → 1

Veralteter State

Erklärung von Beispiel 2:

Unterschied zwischen Funktionskomponenten und Klassenkomponenten wenn sich Props / State ändern:

  • in Klassenkomponenten werden this.props und this.state durch neue Objekte ersetzt
  • in Funktionskomponenten wird die Komponentenfunktion erneut aufgerufen, und eine neue Closure entsteht, welche die neuen Props / State-Werte enthält

Achtung: in Funktionskomponenten können alte / obsolete Daten in alten Closures weiterbestehen

Veralteter State

Beispiel: fehlerhafter Code, der in einer Closure weiter auf einen veralteten State-Eintrag verweist

function Counter() {
  const [count, setCount] = useState(0);
  function startCounting() {
    setInterval(() => {
      // This innermost function will only be created once.
      // The variable "count" will always refer to the
      //   state from the initial rendering (0)
      setCount(count + 1);
    }, 1000);
  }
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => startCounting()}>start</button>
    </div>
  );
}

Veralteter State

ESLint-Regel, die helfen kann, veraltete Daten in einem Effect-Hook zu erkennen:

react-hooks/exhaustive-deps

(in VS Code, installiere hierfür das ESLint-Plugin)

Veralteter State

mögliche Lösungen (siehe auch: Hinweise in den Linter-Meldungen):

  • Ãœbergeben einer "Transformations-Funktion" an den State-Setter (die Funktion hat immer Zugriff auf den aktuellsten State)
  • zusätzliches Speichern der aktuellsten Version eines States in einer ref (ist damit auch in älteren Closures verfügbar)

Veralteter State

mögliche Lösung: "State-Transformations-Funktion"

ersetze dies:

setCount(count + 1);

to this:

setCount((c) => c + 1);

Die innere Funktion bekommt immer den aktuellsten Wert übergeben

Veralteter State

mögliche Lösung: zusätzliches Speichern von Daten in einer Ref (wird später genauer erklärt)

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  function startCounting() {
    setInterval(() => {
      countRef.current++;
      setCount(countRef.current);
    }, 1000);
  }
  // ...
}