In React-Anwendungen kann die Initialisierung des Komponentenstatus je nach Komplexität der Logik Auswirkungen auf die Performance haben. Besonders bei rechenintensiven Operationen zur Berechnung des Anfangszustands lohnt es sich, den Initialwert verzögert – also lazy – zu erzeugen. Mit Hilfe eines Initialisierungs-Callbacks lässt sich dieser Vorgang effizient gestalten und unnötige Berechnungen vermeiden.

Grundproblem

Wenn man useState() verwendet, wird der angegebene Initialwert bei jedem Rendering der Komponente ausgewertet. Bei einfachen Werten wie Zahlen oder Strings ist das kein Problem.

JavaScript State Variable
const [counter, setCounter] = useState(0);

Bei aufwändigen Berechnungen oder teuren Operationen kann es jedoch problematisch werden.

Um dies zu verdeutlichen, betrachten wir ein Beispiel, in dem wir den Zustand einer Komponente mit einem Initialwert über eine Funktion setzen.

In diesem Beispiel verwenden wir die Funktion initState(), mit der wir einen initialen Wert generieren und zurückgeben.

In der Funktion zum Aktualisieren des Zustandes handleUpdateListItems() erhöhen wir die Anzahl der Elemente um 1.

Die Logs in der DevTools-Konsole sollen und dabei helfen zu sehen, wie oft die Funktion initState() aufgerufen wird.

TypeScript StateExample.jsx
import { useState } from 'react';

const InitialStateInefficient = () => {
    const initState = () => {
        let newItems = [];
        for (let i = 0; i < 100; i++) {
            newItems.push(i);
        }
        console.log('Init state', newItems.length);
        return newItems;
    };

    const [listItems, setListItems] = useState(initState());

    const handleUpdateListItems = () => {
        setListItems(prevList => {
            const newList = [...prevList];
            newList.push((newList.length - 1) + 1);
            console.log('New state', newList.length);
            return newList;
        });
    };

    return (
        <>
            <button onClick={handleUpdateListItems}>Add Item</button>
        </>
    );
};

export default InitialStateInefficient;

Wie auf dem folgenden Screenshot zu sehen, wird die Funktion initial aufgerufen, was ganz normal und klar ist. Aber, sie wird auch bei jedem Update des Zustandes erneut ausgeführt. Dabei ist aber die Funktion zur Generierung des initialen Zustandes für uns nicht mehr wichtig. Der initiale Zustand wurde bereits generiert. Hier geht es nur noch um die Aktualisierung des States.

React State - Konsole Ausgabe vom Component mit ineffizienter Nutzung

Wenn man sich nun vorstellt, dass diese Operation eine spürbare Zeit in Anspruch nimmt, weil beispielsweise Daten geladen oder komplexere Berechnungen ausgeführt werden, wird es klar, dass das kein optimaler Weg ist, die Funktion, welche für das Setzen des Initialzustandes gedacht war, bei jeder Aktualisierung auszuführen.

Initialer Zustand - Korrekte Vorgehensweise

React bietet für dieses Problem ein spezielles Muster. Man kann anstelle eines direkten Wertes eine Funktion übergeben, die nur beim ersten Rendering ausgeführt wird.

Wir schreiben also das Beispiel von oben entsprechend um und schauen, wie sich nun das Verhalten verändert hat.

TypeScript StateExample.jsx
import { useState, useEffect } from 'react';

const InitialStateEfficient = () => {

    const initState = () => {
        let newItems = [];
        for (let i = 0; i < 100; i++) {
            newItems.push(i);
        }
        console.log('Init state', newItems.length);
        return newItems;
    };

    // [!code highlight]
    const [listItems, setListItems] = useState(() => initState());

    const handleUpdateListItems = () => {
        setListItems(prevList => {
            const newList = [...prevList];
            newList.push((newList.length - 1) + 1);
            console.log('New state', newList.length);
            return newList;
        });
    };

    return (
        <>
            <button onClick={handleUpdateListItems}>Add Item</button>
        </>
    );
};

export default InitialStateEfficient;

Wenn wir uns jetzt das Verhalten anschauen, stellen wir fest, dass die Funktion zum Initialisieren zu Beginn weiterhin regulär ausgeführt wird.

React State - Initiale Werte beim Beispiel für effiziente Nutzung

Vor dem Klick auf den Button "Add Item" habe ich die Konsole geleert. Nach dem Klick auf den Button sieht man, dass nur der Log mit den aktualisierten Werten in der Konsole erscheint. Die Funktion initState() wird also bei dieser Verwendung nicht erneut ausgeführt.

React State - Aktualisierte Werte beim Beispiel für effiziente Nutzung

Klassische Anwendungsfälle

Lazy Initialization lohnt sich überall, wo der Initialwert teuer zu berechnen ist:

TypeScript UseCases.jsx
// 1. localStorage lesen + JSON.parse
const [user, setUser] = useState(() => {
    const stored = localStorage.getItem('user');
    return stored ? JSON.parse(stored) : null;
});

// 2. Große Datenstruktur aus Props ableiten
const [tree, setTree] = useState(() => buildTreeFromFlatList(props.items));

// 3. Komplexe Initial-Berechnung
const [board, setBoard] = useState(() => Array(64).fill().map(() => ({ piece: null })));

// 4. crypto.randomUUID() — soll nur einmal generiert werden
const [sessionId] = useState(() => crypto.randomUUID());

Bei einfachen Werten wie useState(0), useState(''), useState(false) ist Lazy unnötig — die Auswertung kostet nichts.

Initializer bekommt KEINE Argumente

Eine Falle: der Initializer hat keine Parameter. Wer Props oder andere Werte als „Argument" durchreichen will, nutzt einen Closure:

TypeScript ClosureArgument.jsx
const PropsBased = ({ initialCount }) => {
    // Closure über initialCount
    const [count, setCount] = useState(() => initialCount * 2);
    // …
};

Wichtig: der Initializer-Wert wird beim ERSTEN Render ausgewertet. Wenn initialCount sich später ändert, hat das KEINEN Einfluss auf den State — der ist mit dem damaligen Wert eingefroren. Wer das will: key-Prop auf der Komponente nutzen, um beim Wechsel ein Re-Mount zu erzwingen.

Wann LIEBER nicht Lazy?

  • Wenn der Initialwert ein Primitive ist (Number, String, Boolean) — die Funktions-Overhead frisst die Ersparnis.
  • Wenn die Berechnung Nebenwirkungen hat — die gehören in useEffect, nicht in den Initializer.
  • Wenn der Wert eine Server-Antwort ist — die kommt asynchron, nicht beim Mount. Default null, dann via useEffect setzen.
TypeScript NotLazy.jsx
// ÜBERFLÜSSIG — Primitive ist trivial
const [count, setCount] = useState(() => 0);  // gleich: useState(0)

// FALSCH — fetch ist asynchron, Initializer ist sync
const [user, setUser] = useState(() => fetch('/me')); // Liefert ein Promise als State!

// KORREKT — useEffect für Daten-Laden
const [user, setUser] = useState(null);
useEffect(() => {
    fetch('/me').then(r => r.json()).then(setUser);
}, []);

Interessantes

useState(funktion) ruft die Funktion beim Mount auf — das ist die Lazy-Form.

useState(() => berechne()) ruft berechne() beim ERSTEN Render auf, nicht bei jedem. useState(berechne()) dagegen ruft berechne() bei jedem Render, der Rückgabewert wird nur beim ersten verwendet — wird also unnötig oft ausgeführt.

Lazy-Initializer hat KEINE Argumente.

Wer Werte hineingeben will: in den Closure. useState(() => berechne(props.id)) — die Funktion schließt props.id ein, der Wert von damals wird genutzt.

Bei Primitives bringt Lazy nichts.

useState(() => 0) ist nicht effizienter als useState(0) — eher Overhead durch den Function-Call. Lazy lohnt sich nur bei teurer Berechnung.

Initializer ist synchron — kein async/await möglich.

useState(async () => ...) liefert ein Promise als State, nicht den await-Wert. Async-Initialisierung gehört in useEffect mit Default-Initial-State.

Props-Wert im Initializer eingefroren beim ersten Mount.

Ändern sich nach Mount die Props, hat das KEINEN Einfluss auf den State. Wenn der State sich an Props koppeln soll: useEffect mit Props in Dependencies, oder key-Prop für Re-Mount.

Lazy ist die richtige Form für localStorage-State.

useState(() => JSON.parse(localStorage.getItem('x') ?? '&#123;&#125;')) liest und parst nur beim Mount. Bei jedem Render wäre das verschwendete CPU-Zeit.

StrictMode ruft Initializer in Dev zweimal auf — Side-Effects vermeiden.

React 18 StrictMode mountet Komponenten doppelt, um Bugs aufzudecken. Der Initializer feuert dabei zweimal. Wer dort einen Side-Effect macht (z.B. Logging einer Session-ID), bekommt zwei Logs. Daher: Initializer rein und seiteneffekt-frei halten.

Selbst-Referenzen über stable IDs: useState statt useRef.

Eine einmalig generierte ID, die zwischen Renders stabil bleibt, kann mit Lazy-useState einmalig generiert werden: const [id] = useState(() => crypto.randomUUID()). useRef wäre auch möglich, aber useState ist hier semantisch klarer („ein Wert, der die ganze Lebenszeit gilt").

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu State

Zur Übersicht