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.
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.
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.

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.
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.

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.

Klassische Anwendungsfälle
Lazy Initialization lohnt sich überall, wo der Initialwert teuer zu berechnen ist:
// 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:
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 viauseEffectsetzen.
// Ü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') ?? '{}')) 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
- useState – Avoiding recreating the initial state – react.dev
- useEffect – react.dev
- StrictMode – react.dev