So beheben Sie „Maximum Update Depth Exceeded“ in React
Beheben Sie den React-Fehler maximum update depth exceeded mit Ein-Zeilen-Fixes für Render-Schleifen, Effect-Dependencies, Handler und Throttling.
„Maximum update depth exceeded” bedeutet, dass Ihre Komponente in einer Endlosschleife beim Rendern feststeckt: Ein State-Update löst ein Re-Rendering aus, das wiederum dasselbe State-Update auslöst. React bricht nach mehr als 50 verschachtelten Updates ab (das sogenannte NESTED_UPDATE_LIMIT), damit der Browser nicht einfriert.
Wer schon einmal zugesehen hat, wie sich der Tab aufhängt, während sich dieselbe rote Zeile in der Konsole stapelt, weiß, wie wenig hilfreich diese Textwand zunächst wirkt. Die gute Nachricht: Die Lösung besteht fast immer aus einer einzigen Zeile Code – sobald Sie erkannt haben, in welches Muster Sie geraten sind.
Dieser Leitfaden deckt sämtliche Ursachen ab, einschließlich des Falls hochfrequenter Handler, jeweils mit einem kopierfertigen Vorher/Nachher-Beispiel, einer Symptomtabelle zur schnellen Einordnung sowie dem Werkzeug, mit dem Sie den verantwortlichen Setter in weniger als einer Minute finden. Die Ursache ist bei Function- und Class-Komponenten identisch: eine Update-Schleife, die sich nie stabilisiert.
Die wichtigsten Erkenntnisse
- Der Fehler ist eine Endlosschleife beim Rendern; React stoppt sie, sobald die Anzahl verschachtelter Updates 50 übersteigt (
NESTED_UPDATE_LIMITim Quellcode des Reconcilers). onClick={handleClick()}ruft die Funktion während des Renderns auf und plant bei jedem Render ein State-Update ein. Übergeben Sie stattdessenonClick={handleClick}bzw.onClick={() => handleClick(id)}, wenn Sie Argumente benötigen.- Die am universellsten einsetzbare Lösung ist ein funktionales Update (
setCount(prev => prev + 1)). Damit können Sie diesen State aus dem Dependency-Array des Effects entfernen und durchbrechen so die Lese-dann-Schreib-Schleife. useCallbackstabilisiert die Identität einer Funktion, nicht ihre Aufrufhäufigkeit. Schleifen durch hochfrequente Handler wieonScrolloderonDragMovevon dnd-kit lassen sich damit also nicht beheben – drosseln (throttle) oder entprellen (debounce) Sie stattdessen das State-Update.- Der
#185-Fehler bei Class-Komponenten wird sowohl in der Entwicklungs- als auch in der Produktionsumgebung geworfen, dieuseEffect-Variante ist dagegen nur eine Dev-Warnung. In Produktion läuft diese Schleife ohne geworfenen Fehler und ohne jedes Konsolensignal.
Nachschlagetabelle: Symptom → Ursache → Lösung
Suchen Sie das Symptom, das zu Ihrer Beobachtung passt, und springen Sie dann zur passenden Lösung.
| Symptom | Ursache | Lösung |
|---|---|---|
| Schleife ohne Klick, startet beim Mounten | onClick={fn()} ruft den Setter während des Renderns auf | onClick={fn} übergeben |
setState auf oberster Ebene der Komponente | State-Update im Render-Pfad | In einen Handler oder Effect verschieben |
| Effect läuft bei jedem Render | Fehlende/falsche Dependencies oder Inline-Objekt/-Array in den Dependencies | Dependencies korrigieren + useMemo/useCallback |
| Effect liest und schreibt denselben State | State steht in seinen eigenen Dependencies | Funktionaler Updater; Dependency entfernen |
| Schleife nur beim Ziehen/Scrollen/Skalieren | setState bei jedem hochfrequenten Event | Update drosseln/entprellen |
| Parent und Child überschreiben sich gegenseitig | Bidirektionale State-Synchronisierung | Datenfluss unidirektional gestalten |
Discover how at OpenReplay.com.
Lösung 1 & 2: Handler-Referenzen und setState im Render-Pfad
Die beiden schnellsten Erfolge betreffen die Art, wie Sie Handler verdrahten, und die Stelle, an der Sie den Setter aufrufen. onClick={handleClick()} ruft die Funktion während des Renderns auf und plant bei jedem Render ein State-Update ein; in nahezu allen Fällen wollen Sie onClick={handleClick} bzw. onClick={() => handleClick(id)}, wenn Sie Argumente übergeben müssen.
// BAD: acceptTerms runs during render, every render
<input type="checkbox" onChange={acceptTerms()} />
// GOOD: pass the reference; wrap in an arrow to pass args
<input type="checkbox" onChange={acceptTerms} />
<button onClick={() => selectItem(item.id)}>Select</button>
Dieselbe Schleife entsteht, wenn Sie einen Setter direkt im Komponentenrumpf aufrufen. State-Updates gehören in einen Event-Handler oder einen Effect – niemals in den Render-Pfad.
// BAD: runs on every render → loop
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
// GOOD: update in response to an event
const increment = () => setCount(c => c + 1);
Lösung 3, 4 & 5: Dependency-Schleifen in Effects
Die meisten Effect-Schleifen entstehen durch Dependencies, deren Identität sich ändert, oder durch einen Effect, der genau den State schreibt, den er liest. Ein Objekt- oder Array-Literal, das inline in einem Dependency-Array steht, erhält bei jedem Render eine neue Identität – der Effect läuft also bei jedem Render erneut. Kapseln Sie es in useMemo (Objekte/Arrays) bzw. useCallback (Funktionen), damit die Referenz stabil bleibt.
// BAD: options is a new object each render → effect re-runs forever
const options = { limit: 10, sort: 'date' };
useEffect(() => { search(query, options).then(setResults); }, [query, options]);
// GOOD: memoize so the reference is stable
const options = useMemo(() => ({ limit: 10, sort: 'date' }), []);
Die am universellsten einsetzbare Lösung ist ein funktionales Update. Wenn ein Effect denselben State liest und schreibt, liest setCount(prev => prev + 1) den vorherigen Wert aus dem Updater-Argument statt aus der Closure. Dadurch können Sie diesen State aus dem Dependency-Array entfernen und die Schleife wird durchbrochen.
// BAD: count is read and written, and it's in deps
useEffect(() => { setCount(count + 1); }, [count]);
// GOOD: functional updater removes the dependency
useEffect(() => { setCount(prev => prev + 1); }, []);
Bei einer Funktion, von der ein Effect abhängt, haben Sie zwei Möglichkeiten: Kapseln Sie sie mit korrekten Dependencies in useCallback oder verschieben Sie sie in den Effect hinein – eine innerhalb des Effects deklarierte Funktion wird pro Durchlauf einmal erzeugt und muss keine Dependency sein.
Lösung 6: Hochfrequente Handler drosseln
useCallback stabilisiert die Identität einer Funktion über Renderzyklen hinweg, ändert aber nichts daran, wie oft die Funktion ausgeführt wird. Eine Endlosschleife durch einen hochfrequenten Handler wie onScroll, onMouseMove, onResize oder onDragMove von dnd-kit lässt sich damit also nicht beheben. Diese Events feuern dutzende Male pro Sekunde, und jedes setState plant ein weiteres Rendering ein. Drosseln oder entprellen Sie stattdessen das State-Update.
// BAD: fires dozens of times/sec while dragging
const handleDragMove = (event) => setDragPreview(compute(event));
// GOOD: cap the update rate; useCallback alone won't help
import { throttle } from 'lodash';
const handleDragMove = throttle((event) => setDragPreview(compute(event)), 100);
Das throttle von lodash begrenzt, wie oft die gekapselte Funktion innerhalb eines bestimmten Zeitfensters ausgelöst werden kann; das native requestAnimationFrame oder ein Debounce funktionieren ebenfalls. Entscheidend ist die Frequenzkontrolle, nicht die Referenzstabilität.
Lösung 7: Synchronisierungsschleifen zwischen Parent und Child
Bidirektionale State-Propagierung – ein Child-Effect ruft den Setter des Parents auf, dieser rendert das Child neu, wodurch der Effect erneut ausgelöst wird – ist ein weiterer Klassiker unter den Schleifen. Heben Sie den State auf einen einzigen Besitzer an und halten Sie den Datenfluss unidirektional, oder transformieren Sie den Wert im Parent, statt ihn über einen Effect zurückzusynchronisieren.
Schnell aufspüren (und der Fall der Class-Komponenten)
Arbeiten Sie von oben nach unten: Lesen Sie den Stack-Trace bis zum benannten Setter am Anfang der Schleife und öffnen Sie dann den Profiler der React DevTools, um die Komponente zu finden, die ohne Unterbrechung neu rendert. Ergänzen Sie why-did-you-render (getestet mit React 19, nur für die Entwicklungsumgebung, nicht mit dem React Compiler getestet), um zu sehen, welche Prop oder welcher State die Identität gewechselt hat. Fangen Sie diese Fälle bereits beim Linting ab: Seit eslint-plugin-react-hooks 7.x bringt das Plugin die dedizierten Regeln set-state-in-render und set-state-in-effect mit, die die beiden häufigsten Auslöser erkennen, noch bevor Sie die App starten. Beide sind Teil des Standard-Presets recommended, ein Upgrade des Plugins genügt also, um sie zu aktivieren; recommended-latest legt lediglich zusätzlich die experimentellen Compiler-Regeln darüber. set-state-in-render greift, wenn eine Komponente während des Renderns State setzt, ohne dass der Aufruf durch eine Bedingung abgesichert ist – genau das Muster, das in einer Schleife eskaliert.
Der Fehler hat bei Function- und Class-Komponenten dieselbe Ursache; bei Class-Komponenten bedeutet er meist, dass setState während des Renderns oder bedingungslos innerhalb von componentDidUpdate aufgerufen wird. Achten Sie außerdem auf die Umgebung: Der #185-Fehler bei Class-Komponenten wird sowohl in der Entwicklung als auch in der Produktion geworfen, die useEffect-Variante ist dagegen lediglich eine Warnung in der Entwicklungsumgebung. Im Quellcode des Reconcilers steckt die Prüfung auf verschachtelte Passive Updates in einem Guard, der nur in der Entwicklungsumgebung greift, und schreibt in die Konsole, statt einen Fehler zu werfen. Ein Production-Build führt diese Effect-Schleife somit ohne geworfenen Fehler, ohne Error Boundary und ohne Konsolensignal aus – bemerkbar macht sie sich nur durch einen eingefrorenen Tab.
Genau diese Lücke in der Produktion macht das Debuggen der Schleife teuer. Die Konsole zeigt zwar den Fehler, aber nicht, welche Interaktion ihn ausgelöst hat – und bei Effect-Schleifen zeigt sie unter Umständen gar keinen Fehler. Session-Replay-Werkzeuge wie OpenReplay erfassen den Konsolenfehler, sofern vorhanden, zusammen mit der Abfolge der vorausgegangenen Nutzeraktionen. So wird aus einem nackten Stack-Trace (oder einem stillen Einfrieren) ein reproduzierbarer, Klick für Klick nachvollziehbarer Fall, den Sie gegen die obigen Lösungen abspielen können.
Sobald Sie Ihr Symptom einer Zeile in der Tabelle zugeordnet haben, ist die Änderung meist eine einzige Zeile: eine Referenz statt eines Aufrufs, ein useMemo um ein Objekt oder ein funktionaler Updater, der eine Dependency überflüssig macht. Richten Sie exhaustive-deps zusammen mit den neueren set-state-in-*-Regeln ein, damit die nächste Schleife an Ihrem Lint-Schritt scheitert statt an den Browsern Ihrer Nutzer.
FAQs
Was ist der Unterschied zwischen der useEffect-Variante dieses Fehlers und React-Fehler #185?
Es handelt sich um zwei unterschiedliche Meldungen mit unterschiedlichem Laufzeitverhalten. Fehler #185 ist die Formulierung für Class-Komponenten und verweist auf setState innerhalb von componentWillUpdate oder componentDidUpdate; er wird sowohl in der Entwicklung als auch in der Produktion geworfen. Die useEffect-Variante ist eine eigenständige Warnung aus dem Quellcode des Reconcilers, die nur in der Entwicklungsumgebung ausgegeben wird, da sie hinter einem entsprechenden Guard steht. In Produktion läuft die Effect-Schleife daher ohne geworfenen Fehler, ohne Error Boundary und ganz ohne Konsolensignal.
Warum stoppt useCallback meine Endlosschleife beim Ziehen oder Scrollen nicht?
Weil useCallback die Identität einer Funktion über Renderzyklen hinweg stabilisiert, aber nicht beeinflusst, wie oft diese Funktion ausgeführt wird. Ein hochfrequenter Handler wie onScroll, onMouseMove oder onDragMove von dnd-kit feuert dutzende Male pro Sekunde, und jedes setState plant ein weiteres Rendering ein – unabhängig davon, ob die Funktionsreferenz memoisiert ist. Die Lösung ist Frequenzkontrolle: Drosseln oder entprellen Sie das State-Update selbst, etwa mit throttle aus lodash, einem Debounce oder requestAnimationFrame.
Erlaubt mir die funktionale Updater-Form immer, State aus dem Dependency-Array eines Effects zu entfernen?
Nur dann, wenn der Effect diesen State ausschließlich zur Berechnung des nächsten Werts benötigt. setCount(prev => prev + 1) liest den vorherigen Wert aus dem Updater-Argument statt aus der Closure, sodass count das Dependency-Array verlassen kann und die Lese-dann-Schreib-Schleife durchbrochen wird. Liest der Effect diesen State auch für andere Logik – etwa für Verzweigungen oder zur Übergabe an eine andere Funktion –, benötigen Sie ihn weiterhin als Dependency und müssen die Schleife anders auflösen.
Warum erscheint der Fehler in der Entwicklung, während mein Production-Build einfach still einfriert?
Weil der Guard für verschachtelte Passive Updates bei der useEffect-Variante im React-Reconciler in eine Prüfung gekapselt ist, die nur in der Entwicklungsumgebung greift – die Warnung wird also ausschließlich während der Entwicklung in der Konsole ausgegeben. In einem Production-Build läuft dieser Guard nicht, das heißt, dieselbe Effect-Schleife wird ohne geworfenen Fehler und ohne Konsolenmeldung ausgeführt und zeigt sich nur als eingefrorener Tab oder als unkontrolliert laufende Renderings. Der #185-Fehler bei Class-Komponenten ist ein anderer Fall und wird in beiden Umgebungen geworfen.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k