Startseite / Artikel

Warum push() Ihre React-Oberfläche veraltet macht und wie neue Referenzen das beheben

Verstehen Sie, warum das Ändern von Arrays in useState niemals eine Neuansicht auslöst, wie die Referenzprüfung bei React funktioniert und wann Spread-Kopien sowie funktionale Updates verwendet werden sollten.

Verlag: REACTAPP.TOP6 Min. Lesezeit1280 Wörter

Quellen: React Survival Guide, Part 2: Don’t Mutate! Why `.push()` is Killing Your UI

Nur wenige React-Bugs sind so verwirrend wie eine Benutzeroberfläche, die sich weigert, sich zu aktualisieren, obwohl die Konsole darauf hinweist, dass die Daten korrekt sind. Es gibt weder Fehler noch Warnungen – nur ein Bildschirm, der bei alten Werten stecken bleibt. In der überwiegenden Mehrheit der Fälle liegt die Ursache in einer Zustandsmutation: Man ändert ein Array oder ein Objekt vor Ort anstelle dessen, es durch ein neues zu ersetzen. Dieser Artikel erklärt genau, warum React Änderungen vor Ort ignoriert, zeigt die einzeilige Lösung und behandelt die funktionale Aktualisierungsform, die nötig ist, wenn sich die Updates schnell häufen.

Der Bug: Die Daten ändern sich, der Bildschirm nicht

Stellen Sie sich einen kleinen Warenkorb, eine To-Do-Liste oder eine Benutzerliste vor. Die letzte Aufgabe besteht darin, ein Element in ein Array hinzuzufügen, das über useState verwaltet wird, und der erste Versuch sieht aus wie gewöhnliches JavaScript.

const [cart, setCart] = useState(['Coffee']);

const addItem = (newItem) => {
  // It's just JavaScript, right?
  cart.push(newItem);
  console.log("Added! Cart is now:", cart);
};

Nach dem Klicken auf „In den Warenkorb hinzufügen“ zeigt das Protokoll ['Coffee', 'Tea'], sodass die Daten scheinbar korrekt sind. Der angezeigte Warenkorb listet jedoch weiterhin nur „Coffee“ auf. Die Benutzeroberfläche zeigt den Nutzern somit etwas an, das nicht mehr zutrifft – was schlimmer ist als ein Absturz, da niemand etwas bemerkt, bis ein Kunde es feststellt. Entwickler verbringen in solchen Fällen oft eine Stunde damit, Schleifen und Render-Logik erneut zu überprüfen, obwohl diese nie das Problem waren.

Bemerken Sie, dass dieser Handler überhaupt nicht auf setCart aufruft. React wurde nicht mitgeteilt, dass etwas geschehen ist, weshalb es keinen Grund hatte, etwas anzuzeigen. Eine subtilere Variante des Fehlers fügt zwar setCart(cart) nach dem push() ein, doch der Bildschirm ändert sich dennoch nicht. Im nächsten Abschnitt wird erklärt, warum das so ist.

React vergleicht Referenzen, nicht Inhalte

Die wichtigste Idee, die man verinnerlichen muss, ist einfach: React verfolgt die Identität, nicht den Inhalt.

Methoden wie .push(), .pop(), .splice() und .unshift() oder eine Zuweisung wie user.name = 'Alice' sind Mutationen. Sie weisen JavaScript an, den Wert an einer vorhandenen Stelle im Speicher zu ändern. JavaScript tut dies umgehend, weshalb console.log(cart) den neuen Eintrag anzeigt. React überwacht diesen Speicher jedoch nicht. Sein Aktualisierungsmodell geht davon aus, dass der Zustand unveränderlich ist.

Wenn Sie einen State-Setter aufrufen, vergleicht React den neuen Wert mit dem aktuellen mithilfe von Object.is. Bei Arrays und Objekten handelt es sich dabei um einen Referenzvergleich: Es wird nur geprüft, ob es sich um dasselbe Objekt wie zuvor handelt, nicht ob dessen Inhalt unterschiedlich ist. Nach einem push() ist das zurückgegebene Array dasselbe Objekt, das React bereits besitzt. Die Überprüfung ergibt „unverändert“, React überspringt die Neuzeichnung, und die Benutzeroberfläche zeigt weiterhin die zuletzt gespeicherte Version an.

Diese günstige Überprüfung ist beabsichtigt. Der Vergleich von Referenzen dauert unabhängig von der Größe des States konstant lange, während ein tiefer Vergleich jedes eingebetteten Wertes bei jeder Aktualisierung aufwändig wäre. Der Kompromiss besteht darin, dass Sie Änderungen durch Bereitstellung einer neuen Referenz signalisieren müssen.

Die Lösung: Kopieren, den Kopiervorgang ändern, den Kopiervorgang setzen

Immutabilität klingt anspruchsvoller, als sie tatsächlich ist. Um ein Array oder ein Objekt im Zustand zu aktualisieren, sollte man das ursprüngliche unverändert lassen, ein neues erstellen, das die Änderung enthält, und dieses dem Setter übergeben. In modernem JavaScript ist der Spread-Operator (...) die schnellste Methode dafür.

Die korrigierte addItem-Funktion erstellt ein neues Array, das die alten Elemente gefolgt von dem neuen Element enthält.

const addItem = (newItem) => {
  // The right way (creating a new identity):
  setCart([...cart, newItem]); // Create a new array, add newItem at the end.
};

Nun erhält der Setter eine andere Referenz, die Object.is-Prüfung fehlschlägt, und React lädt die Komponente erneut mit dem hinzugefügten Element. Dasselbe Prinzip gilt für andere Operationen:

  • Löschen: cart.filter(item => item !== target) anstelle von splice().
  • Ersetzen: cart.map(item => (item.id === id ? { ...item, qty: item.qty + 1 } : item)).
  • Objekt aktualisieren: setUser({ ...user, name: 'Alice' }) anstelle der Zuweisung an eine Eigenschaft.
  • Sortieren oder umkehren: Erst mit [...cart].sort() eine Kopie anfertigen, oder die nicht-mutierenden Methoden toSorted() und toReversed() verwenden, sofern die Zielbrowser sie unterstützen.
  • Beachten Sie, dass das Spread-Operator nur eine Ebene kopiert. Wenn der Zustand verschachtelte Objekte enthält, benötigt jede geänderte Ebene ihre eigene Kopie. Für eine ausführlichere Betrachtung der Referenzmutation außerhalb des React-Zustands, einschließlich verschachtelter Strukturen, siehe Vermeidung stummer Zustandsfehler durch JavaScript-Referenzmutation.

    Vorteile immutabler Updates

    Das ist nicht nur eine von React auferlegte Regel; dadurch entsteht Code, der leichter zu verstehen ist.

    1. Klare Aktualisierungspunkte. Jede Zustandsänderung erfolgt über einen Setter, sodass deutlich ist, welche Funktionen den Zustand ändern und wo.
    2. Besseres Debuggen. Da jede Aktualisierung einen eindeutigen Wert erzeugt, können React DevTools sowie andere Werkzeuge den Zustand vor und nach jeder Änderung anzeigen.
    3. Keine versteckten Nebeneffekte. Da Daten niemals vor Ort geändert werden, kann ein Fehler in einem Komponenten nicht heimlich einen Array oder ein Objekt beschädigen, das noch von einem anderen Teil der Anwendung gelesen wird.

    Funktionale Aktualisierungen für schnelle oder überschneidende Änderungen

    Ein weiteres, damit verbundenes Problem tritt bei hochfrequenten Aktualisierungen auf, wie zum Beispiel schnellen wiederholten Klicks oder mehreren asynchronen Antworten, die nahe beieinander eintreffen – möglicherweise auch aus dem falschen Ordner. Jeder Handler greift den Zustandswert aus der Render-Schleife auf, in der er erstellt wurde. Wenn zwei Aktualisierungen vor einer erneuten Renderung ausgeführt werden, können beide von demselben veralteten Zustandsbild ausgehen, wodurch eine Änderung die andere überschreibt. Dies ist das Problem der veralteten Schließfunktionen, und es beeinflusst direkt die Art und Weise, wie Sie neuen Zustand erstellen.

    Die Lösung besteht darin, eine Funktion an den Setter zu übergeben. React ruft diese Funktion mit dem neuesten, noch nicht verarbeiteten Zustand auf, sodass jede Aktualisierung auf dem Ergebnis der vorherigen basiert und nicht auf einem gespeicherten Wert. Das untenstehende Beispiel fügt eine Gruppe von generierten Elementen hinzu. Es liest prevCart.length aus dem Argument, um diese zu nummerieren, und gibt anschließend ein völlig neues Array zurück, das die vorherigen sowie die neuen Elemente enthält.

    // A very fast counter:
    const addManyItems = (count) => {
      // Good: Uses spread to be immutable
      // Better: Uses functional update for consistency
      setCart(prevCart => {
        // We are generating new array content:
        const newItems = Array.from({ length: count }, (_, i) => `Item ${prevCart.length + i + 1}`);
        // Spread operator for the final, clean, new identity:
        return [...prevCart, ...newItems];
      });
    };
    

    Zwei Faktoren wirken hier zusammen. Die Spread-Operation erzeugt weiterhin eine neue Referenz, wodurch React neu rendern muss. Die Aktualisierungsfunktion stellt sicher, dass das Basisarray immer das aktuellste ist, was verhindert, dass Updates verloren gehen. Eine gute Gewohnheit ist es, die funktionale Form zu verwenden, sobald der nächste Zustand aus dem vorherigen Zustand berechnet wird.

    Zusammenfassung

    • Eine Mutation ändert die Daten, aber nicht die Referenz; daher kommt Reacts Object.is-Prüfung zu dem Schluss, dass sich nichts geändert hat, und überspringt das Rendering.
    • Falls die Benutzeroberfläche hinter den Daten zurückbleibt, suchen Sie zunächst nach .push(), .unshift(), .splice() oder state.property = value im Zustand.
    • Erstellen Sie mit Spread, map oder filter ein neues Array oder Objekt und kopieren Sie jede veränderte verschachtelte Ebene.
  • Verwenden Sie den funktionalen Updater, wenn der neue Zustand vom alten Zustand abhängt, insbesondere bei schnellen oder asynchronen Aktualisierungen.
  • Die Lösung erfordert in der Regel nur eine einzige Zeile, und sie macht den Unterschied zwischen einer Schnittstelle, der die Benutzer vertrauen können, und einer, die stumm veraltete Informationen anzeigt.