Du aktualisierst die Seite und der Bildschirm bleibt leer. Oder vielleicht versetzt ein Mausklick deinen CPU-Lüfter in den Überdrehmodus. Dann spuckt die Konsole die gefürchtete Warnung aus: Maximum update depth exceeded. React hat die Notbremse gezogen, weil deine Komponente in einer Endlosschleife gefangen ist. Dies ist einer der häufigsten Fehler in React-Anwendungen und resultiert meist aus einem einfachen Missverständnis darüber, wann dein Code tatsächlich ausgeführt wird.

Um das Problem zu beheben, musst du genau verstehen, in welchem Moment ein Render zu einem Re-Render wird und warum State-Änderungen außerhalb des Render-Pfads bleiben müssen.

Wie React deine Komponente rendert

React baut Benutzeroberflächen aus Komponenten auf. Im modernen React sind diese Komponenten Funktionen. Jedes Mal, wenn React deine Komponente auf dem Bildschirm anzeigen muss, ruft es einfach diese Funktion auf. Innerhalb der Funktion kannst du State verwenden, um Informationen zwischen den Renders zu speichern. Der State teilt React mit, welche Daten zur Komponente gehören und – entscheidend – wann sich etwas geändert hat und die UI aktualisiert werden muss.

Wenn sich der State ändert, plant React ein neues Rendering ein. Die Komponentenfunktion wird erneut ausgeführt, gibt neues JSX zurück und React aktualisiert das DOM entsprechend. Im normalen Gebrauch ist dieser Zyklus harmlos. Du klickst auf eine Schaltfläche, ein Event-Handler aktualisiert den State, React führt ein Re-Render durch und der Benutzer sieht den neuen Text oder die neue Farbe.

Der Fehler tritt auf, wenn ein Render selbst ein weiteres State-Update auslöst. Dieses neue State-Update löst ein weiteres Rendering aus, welches wiederum ein anderes State-Update auslöst. React toleriert dies für ein paar Dutzend Zyklen, bevor es den Fehler „Maximum update depth exceeded“ wirft, um den Browser vor einem vollständigen Einfrieren zu schützen.

Ein kurzer Blick auf den State

Bevor wir die Schleife analysieren, erinnere dich daran, wie der useState-Hook funktioniert. Er gibt dir genau zwei Dinge: eine Variable, die den aktuellen Wert hält, und eine Funktion, um diesen Wert zu ändern.

const MessageComponent = () => {
  const [message, setMessage] = useState('Welcome');

  return <h1>{message}</h1>;
};

Hier ist message beim ersten Render 'Welcome'. Wenn du später setMessage('Goodbye') aufrufst, registriert React die Änderung, ruft MessageComponent erneut auf und die UI zeigt nun „Goodbye“ an. Alles ist in Ordnung, da innerhalb des Komponentenkörpers selbst nichts die Setter-Funktion automatisch aufruft. Die Schleife beginnt, wenn der Setter während der Render-Phase ohne ein externes Ereignis ausgelöst wird.

Direkter Aufruf von SetState im Funktionskörper

Der direkteste Weg, eine Endlosschleife zu erzeugen, besteht darin, eine State-Setter-Funktion direkt im Komponentenkörper aufzurufen. Da der Komponentenkörper bei jedem Render ausgeführt wird, wird der Setter bei jedem Render ausgelöst. Dieses neue State-Update verursacht ein weiteres Rendering. Der Zyklus dreht sich endlos weiter.

So sieht der Fehler aus:

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

  setCount(count + 1);

  return <div>{count}</div>;
};

Jedes Mal, wenn Counter rendert, erhöht es count. React rendert erneut, um die neue Zahl anzuzeigen, sieht wieder setCount(count + 1) und erhöht den Wert noch einmal. Die Lösung ist einfach: Rufe niemals einen State-Setter auf oberster Ebene deiner Komponente während des Renders auf. State-Updates sollten auf Benutzerereignisse oder Side Effects reagieren und nicht auf den Akt des Zeichnens des Bildschirms. Verschiebe dieses Update stattdessen in einen Event-Handler:

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

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

Die einzige Ausnahme beim Aufruf von Settern während des Renders ist, wenn du einen neuen State aus Props berechnest. Aber selbst dann solltest du ein anderes Muster verwenden, wie etwa den Wert direkt abzuleiten oder useEffect gezielt einzusetzen.

Übergabe eines Funktionsaufrufs statt einer Referenz

Eine weitere häufige Ursache ist ein subtiler JSX-Tippfehler. Wenn du einen Event-Handler anhängst, musst du die Funktion selbst übergeben. Wenn du die Funktion versehentlich direkt im JSX aufrufst, wird sie sofort während des Render-Zyklus ausgeführt.

const Toggle = () => {
  const [isOn, setIsOn] = useState(false);

  const handleToggle = () => setIsOn(!isOn);

  return <button onClick={handleToggle()}>Toggle</button>;
};

Indem du handleToggle() mit Klammern schreibst, übergibst du React keine Funktion, die später beim Klicken des Benutzers aufgerufen werden kann. Du rufst sie genau jetzt auf, während React das virtuelle DOM aufbaut. Da handleToggle den State aktualisiert, rendert die Komponente neu. Während dieses Re-Renders sieht React handleToggle() erneut und ruft es wieder auf. Die Schleife endet nie. Die korrekte Version entfernt die Klammern:

return <button onClick={handleToggle}>Toggle</button>;

Wenn du Argumente übergeben musst, umschließe den Aufruf mit einer anonymen Funktion:

return <button onClick={() => handleToggle(true)}>Switch On</button>;

Diese Unterscheidung bringt selbst erfahrene Entwickler beim Refactoring durcheinander. Achte auf diese Klammern.

Die useEffect-Dependency-Falle

Effects sind der richtige Ort für Side Effects wie das Abrufen von Daten, die Synchronisierung mit Browser-APIs oder die manuelle Manipulation des DOMs. Aber useEffect wird ausgeführt, nachdem React das Rendering auf dem Bildschirm committet. Wenn dein Effect den State aktualisiert, wird React ein Re-Render durchführen. Normalerweise ist das völlig in Ordnung. Es wird jedoch zu einer Schleife, wenn der Effect nach jedem Render ausgeführt wird und immer denselben State aktualisiert.

Consider this broken pattern:

const UserProfile = () => {
  const [user, setUser] = useState({});

  useEffect(() => {
    setUser({ name: 'Ada', role: 'Admin' });
  });

  return <div>{user.name}</div>;
};

Because there is no dependency array, this effect runs after every single render. It sets user, which triggers a render. After that render, the effect runs again and sets user again. React detects the spiral and throws the error.

The fix is to tell React when the effect actually needs to run by supplying a proper dependency array. If the effect should only run once on mount, pass an empty array:

useEffect(() => {
  setUser({ name: 'Ada', role: 'Admin' });
}, []);

If the effect depends on a prop or a piece of state, include only that variable in the array. Be careful, though. Including a variable that changes every render will just recreate the same loop through a different door. For example, if you include an object literal in the dependencies and that object is recreated on every parent render, the effect will fire endlessly. In those cases, you may need to move the object creation outside the component or memoize it.

Practical Debugging Steps

When you hit this error, the stack trace can look overwhelming because React has already repeated the cycle dozens of times. Start by reading the top of the trace to find which component is named repeatedly. Then look for state setters in these three places:

  1. The main body of the component, outside any handler or hook.
  2. JSX event attributes where you might have written handler() instead of handler.
  3. useEffect hooks that lack a dependency array or depend on unstable references.

Temporarily comment out each state setter until the error stops. That tells you exactly which update is the culprit. If the setter is inside an effect, ask yourself whether you even need state there. Sometimes developers set local state from props inside an effect when they could simply use the prop directly in the JSX.

The Real Takeaway

The Maximum update depth error is not a mysterious React bug. It is a safety net. It means your component is trying to re-render itself instead of waiting for an external signal. Break the habit of treating renders as events that should produce more state. Treat renders as pure consequences of state, not as causes. Keep state updates inside event handlers, callbacks, or effects with carefully chosen dependencies, and you will never see this error again.