Ursachen speichern, Konsequenzen ableiten: Das Entwerfen eines minimalen React-Zustands
Lernen Sie, überflüssigen React-State zu erkennen, Effekt-getriebene Synchronisierungsketten sowie boolesche Flags durch abgeleitete Werte und Status-Unionen zu ersetzen, und entscheiden Sie, wo der State platziert werden sollte.
Die meisten React-Komponenten werden nicht aufgrund einer einzigen falschen Entscheidung schwer zu ändern. Sie entwickeln sich Schritt für Schritt durch jeweils einen scheinbar vernünftigen Aufruf von useState, bis dieselben Informationen an drei verschiedenen Stellen gespeichert sind und niemand mehr sagen kann, welche Kopie die gültige ist. Diese Anleitung geht einer realistischen Produktliste durch, die in diese Falle gerät, und zeigt anschließend, wie man entscheiden kann, was eine Komponente tatsächlich speichern sollte, was sie bei jeder Neuzeichnung berechnen muss und wohin jedes verbleibende Zustandselement gehört. Am Ende haben Sie eine konkrete Überprüfungsliste, um den Zustand zu reduzieren, bevor er zu Synchronisierungsfehlern führt.
Wie eine einfache Produktliste Zustand ansammelt
Stellen Sie sich ein internes Admin-Bildschirm vor, der Produkte auflistet. Die Benutzer können einen Namen eingeben, um zu suchen, die Liste auf eine Kategorie einzugrenzen, nach Preis zu sortieren, ein einzelnes Produkt auszuwählen und zu sehen, wie viele Ergebnisse übrig sind. Die erste Version enthält nur das, was der Benutzer eingegeben und ausgewählt hat:
function Products({ products }) {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
// ...
}
Dann kommen die Anfragen nach zusätzlichen Funktionen. Da die Tabelle die passenden Produkte anzeigen muss, fügt jemand eine Zustandsvariable hinzu, die die gefilterte Liste speichert:
const [filteredProducts, setFilteredProducts] = useState(products);
Ein Zähler über der Tabelle zeigt an, wie viele Produkte passen, und auch diese Zahl erhält einen eigenen Zustand:
const [resultCount, setResultCount] = useState(products.length);
Wenn keine Ergebnisse vorliegen, sollte die Seite eine Meldung zum leeren Zustand anzeigen, wofür ebenfalls ein Flag hinzugefügt wird:
const [hasResults, setHasResults] = useState(true);
Dann folgt die Sortierung, bei der sowohl die gewählte Sortierkriterium als auch eine sortierte Kopie der Liste verwendet werden:
const [sortBy, setSortBy] = useState("name");
const [sortedProducts, setSortedProducts] = useState(products);
Jede Änderung ist klein und leicht in der Überprüfung zu genehmigen. Einige Wochen später beginnen jedoch die Fehlerberichte. Das Wechseln der Kategorien zeigt manchmal die richtigen Zeilen neben der falschen Anzahl an. Das Leeren des Suchfeldes zeigt für einen Moment die Meldung „Keine Ergebnisse“. Wenn eine neue API-Antwort neue Produkte liefert, zeigt die Tabelle weiterhin eine veraltete, gefilterte Liste, bis der Benutzer etwas klickt.
Das Komponente ist voller Zustände, kann aber nicht mehr die eine wichtige Frage beantworten: Welcher dieser Werte ist der echte?
useState ist nicht schuld. Das Problem beginnt, wenn eine Komponente mehrere gespeicherte Versionen von Informationen behält, die alle aus einer kleineren Anzahl grundlegender Fakten berechnet werden könnten. Jeder zusätzlich gespeicherte Wert ist etwas, das im Einklang mit den anderen gehalten werden muss – und genau dabei werden einfache Komponenten anfällig.
Jeder gespeicherte Wert ist eine weitere Möglichkeit, Fehler zu machen
Komponenten benötigen einen Zustand, weil bestimmte Informationen zwischen den Aufrufen überleben müssen: der Text in einem Eingabefeld, die aktive Registerkarte, ob ein Modalfenster geöffnet ist, welche Zeile der Benutzer ausgewählt hat. Das sind natürliche Anwendungsfälle.
Der Fehler besteht darin, „das wird auf dem Bildschirm angezeigt“ so zu interpretieren, als würde das bedeuten „das muss gespeichert werden“. Schauen Sie sich erneut die Produktseite an, bei der alle Werte im Zustand gespeichert sind:
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const [filteredProducts, setFilteredProducts] = useState(products);
const [resultCount, setResultCount] = useState(products.length);
const [hasResults, setHasResults] = useState(true);
Jede Variable hat einen sinnvollen Namen, doch sie sind nicht voneinander unabhängig. Die gefilterte Liste hängt von drei Eingaben ab:
products + search + category
Die Anzahl hängt von der gefilterten Liste ab:
filteredProducts.length
Und die Flagge für den leeren Zustand hängt von der Anzahl ab:
resultCount > 0
Einige echte Fakten wurden in mehrere gespeicherte Folgen umgewandelt. Dadurch entstehen Kombinationen, die die Benutzeroberfläche niemals anzeigen sollte, wie zum Beispiel diese:
filteredProducts = []
resultCount = 4
hasResults = true
React speichert diese Werte gerne. Da Sie drei unabhängige Zustände deklariert haben, behandelt React sie als solche. Es ist ausschließlich Ihre Aufgabe, dafür zu sorgen, dass sie logisch konsistent bleiben, und jeder Event-Handler, der einen dieser Zustände berührt, muss sich an die anderen erinnern.
Die React-Dokumentation rät aus genau diesem Grund von überflüssigem und doppeltem State ab: Wenn ein Wert während der Renderung aus Props oder anderem State berechnet werden kann, führt das Einlagern davon getrennt lediglich zu neuen Möglichkeiten, dass die Kopien nicht übereinstimmen.
Die Botschaft lautet nicht „ruft useState seltener auf“. Es handelt sich vielmehr um eine Veränderung in der Auffassung davon, was als State gilt:
Denken Sie an die Fakten, die die Komponente auf keine andere Weise wiederherstellen kann. Berechnen Sie alles, was daraus folgt.
Bewahren Sie Eingaben im State auf und berechnen Sie den Rest
Die Produktseite musste sich niemals resultCount merken. Was sie sich merken muss, ist, was der Benutzer in das Suchfeld eingegeben hat und welche Kategorie er ausgewählt hat. Das sind Entscheidungen eines Menschen, und nichts in den Produktdaten kann sie rekonstruieren. Alles Weitere ergibt sich daraus:
function Products({ products }) {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const filteredProducts = products.filter((product) => {
const matchesSearch = product.name
.toLowerCase()
.includes(search.toLowerCase());
const matchesCategory =
category === "all" || product.category === category;
return matchesSearch && matchesCategory;
});
const resultCount = filteredProducts.length;
const hasResults = resultCount > 0;
// ...
}
Achten Sie darauf, was verschwunden ist. Es muss nichts mehr resultCount aktualisieren – hasResults verfügt über keinen Setter, und es gibt nicht länger einen Fall, in dem ein Handler die gefilterte Liste aktualisiert, aber die Anzahl vergisst. Jede Darstellung berechnet einfach erneut die Ausgaben auf Basis der aktuellen Eingaben. Ein neuer Suchbegriff erzeugt eine neue Liste, eine neue Kategorie erzeugt ebenfalls eine neue Liste, und wenn der Elternteil ein anderes products-Array überlässt, verwendet der Algorithmus dieses einfach.
Die Komponente verfügt nun über weniger schreibbare Variablen, was eine weitaus bedeutendere Verbesserung darstellt als weniger Zeilen. Ein abgeleiteter Wert kann zwar weiterhin einen Logikfehler enthalten, er kann aber niemals veraltet sein, weil ein Handler vergessen hat, ihn zu aktualisieren. Dadurch entfällt eine ganze Reihe von Zuständen, die die Komponente zuvor erreichen konnte.
In den React-Dokumentationen wird dieselbe Idee mit einem fullName veranschaulicht, das aus Vornamen und Nachnamen gebildet wird: Wenn man es während der Renderung berechnen kann, bringt eine separate Zustandsvariable nichts außer der Möglichkeit zu Inkonsistenzen.
Ein schneller Test, den Sie während der Code-Review anwenden können:
If I deleted this state variable,
could I reconstruct its value exactly
from current props and other state?
Falls die Antwort ja lautet, beginnen Sie damit, es zu einer einfachen Berechnung zu machen. Zustand dient dazu, Informationen zu speichern – nicht dazu, jeden Zwischenergebnis zu cachen, das die Komponente zufällig im Laufe der Ausführung erzeugt.
Wenn useEffect zu einem Synchronisierungsprozess wird
Eine häufige Reaktion auf veralteten abgeleiteten Zustand ist es, useEffect zu nutzen, um eine Kopie automatisch auf dem neuesten Stand zu halten. Die Produktkomponente entwickelt sich dann etwa so weiter:
const [filteredProducts, setFilteredProducts] = useState(products);
useEffect(() => {
const nextProducts = products.filter((product) => {
const matchesSearch = product.name
.toLowerCase()
.includes(search.toLowerCase());
const matchesCategory =
category === "all" || product.category === category;
return matchesSearch && matchesCategory;
});
setFilteredProducts(nextProducts);
}, [products, search, category]);
Dann hält ein zweiter Effect die Anzahl in Einklang mit der Liste:
useEffect(() => {
setResultCount(filteredProducts.length);
}, [filteredProducts]);
Und vielleicht steuert ein dritter Effect das Flag für den leeren Zustand:
useEffect(() => {
setHasResults(resultCount > 0);
}, [resultCount]);
Zusammen bilden sie ein kleines internes Pipeline-System:
products/search/category
↓
filteredProducts
↓
resultCount
↓
hasResults
Keiner dieser Schritte kommuniziert mit etwas außerhalb von React. Sie wandeln lediglich Werte um, die React bereits besitzt – und genau diese Unterscheidung ist der Kern der Sache. Die React-Dokumentation betrachtet Effects als Ausweg, um eine Komponente mit Dingen in Einklang zu bringen, die React nicht kontrolliert, wie beispielsweise Browser-APIs, Sockets oder nicht-React-Widgets. Wenn ein Effect nur dazu dient, einen Bestandteil des Komponentenzustands aufgrund eines anderen zu ändern, lautet die aktuelle Empfehlung, zu prüfen, ob dieser zweite Zustand überhaupt notwendig ist.
Es gibt außerdem eine Laufzeitkosten, die leicht übersehen werden kann. Jeder Effect wird ausgeführt, nachdem React bereits den Render abgeschlossen hat, wodurch jede Verknüpfung in der Kette einen weiteren Render mit teilweise aktualisierten Werten auslöst. Genau daraus resultiert das kurze „Keine Ergebnisse“-Anzeigen im Anfangsszenario: Bei einem Render existiert die neue Liste bereits, doch das Flag spiegelt weiterhin die alte Anzahl wider.
Die berechnete Version weist überhaupt keine solche Kette auf:
const filteredProducts = filterProducts(
products,
search,
category
);
const resultCount = filteredProducts.length;
const hasResults = resultCount > 0;
Es handelt sich dabei nicht nur um eine ordentlichere Syntax – sie verändert auch, worüber man nachdenken muss. Mit gespeichertem abgeleiteten Zustand überwacht man, wann jeder Wert zuletzt geschrieben wurde, ob der entsprechende Effect bereits ausgeführt wurde, ob sein Abhängigkeitsarray vollständig ist und ob noch weitere Aktualisierungen hinter ihm in der Warteschlange stehen. Bei Berechnungen konzentriert man sich auf Eingaben und Ausgaben, und für reine Transformationen ist das ein weitaus einfacherer Ansatz zur Wartung. Falls Ihre Codebasis bereits Effects dieser Art enthält, erklärt der schrittweise Refactoring-Prozess in Stop Syncing State with useEffect, wie man sie sicher entfernt.
Ersetzen Sie boolesche Flags durch einen einzigen Status
Duplicate-Werte sind eine Form von übermäßigem Zustand. Eine weitere Erscheinungsform tritt auf, wenn ein Konzept über mehrere unabhängige Boolesche Werte verteilt ist. Die Übermittlung eines Formulars ist ein klassisches Beispiel:
const [isIdle, setIsIdle] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Der Ablauf soll sich genau in einer von vier Phasen befinden:
idle
submitting
success
error
Vier Boolesche Werte können jedoch sechzehn Kombinationen darstellen, wobei viele davon sinnlos sind. Die Formulare können gleichzeitig behaupten, eingereicht worden zu sein und bereits erfolgreich zu sein:
isSubmitting = true
isSuccess = true
Oder sie können gleichzeitig Erfolg und Misserfolg melden:
isSuccess = true
isError = true
Oder jeder Flag kann false sein, was keiner der Phasen entspricht. Die Benutzeroberfläche erzeugt diese Kombinationen möglicherweise nie absichtlich, doch die Datenstruktur erlaubt sie – daher reicht bereits ein fehlender Aufruf eines Setters in einem Handler aus, um darauf zuzusteuern. Reacts Leitlinien zur Strukturierung des Zustands empfehlen ausdrücklich, solche Widersprüche zu vermeiden und Variablen einzuschränken, die unmögliche Benutzeroberflächenzustände ermöglichen.
Ein einziger Statuswert beschreibt das Konzept weitaus präziser. In TypeScript ermöglicht eine Union von String-Literalen dem Compiler außerdem, Tippfehler sowie unbekannte Phasen abzulehnen:
type Status =
| "idle"
| "submitting"
| "success"
| "error";
const [status, setStatus] = useState<Status>("idle");
Die praktischen Booleschen Werte sind weiterhin verfügbar, nun als abgeleitete Werte:
const isSubmitting = status === "submitting";
const isSuccess = status === "success";
const isError = status === "error";
Der Unterschied scheint gering, ist aber grundlegend. Die erste Version verlangt von Ihrem Code, vier Fakten in Einklang zu halten. Die zweite speichert ein Fact und liest vier Darstellungen davon.
Der Nutzen nimmt mit der Komponente zu. Ein Checkout könnte diese Phasen durchlaufen:
editing
validating
submitting
confirmed
failed
Ein Dateiimporter könnte ebenfalls diese Phasen durchlaufen:
idle
uploading
processing
completed
failed
Wenn eine Komponente Modi hat, die sich gegenseitig ausschließen, sollte diese Ausschlussregel Teil des Zustandsmodells sein und nicht als Regel gelten, der jeder Handler folgen muss. Sie entscheiden darüber, welche Zustände das Programm überhaupt darstellen kann – und diese Entscheidung verdient genauso viel Sorgfalt wie die Struktur des Codes. Eine Einschränkung: Wenn eine Phase Daten enthält, wie beispielsweise eine Fehlermeldung, die nur in der failed-Phase vorhanden ist, sorgt eine differenzierte Union von Objekten dafür, dass diese Daten der richtigen Phase zugeordnet bleiben, anstatt eine weitere freie Variable hinzuzufügen.
Mehrere kleine Variablen sind nicht dasselbe wie ein großes Objekt
Sobald ein Team den Begriff „Zustand reduzieren“ hört, ist die verlockende Überkorrektur, alles in ein einziges Objekt zu packen:
const [state, setState] = useState({
search: "",
category: "all",
selectedProductId: null,
sidebarOpen: false,
page: 1,
});
Das ist standardmäßig keine Verbesserung. Getrennte useState-Aufrufe haben keinen nennenswerten Aufwand, daher ist es nicht das Ziel, ihre Anzahl zu minimieren. Das Ziel besteht darin, unabhängige Informationen klar darzustellen und zu vermeiden, dieselbe Information zweimal zu speichern.
search und category ändern sich nach ihren eigenen Zeitplänen, und sidebarOpen hat nichts mit beiden zu tun. Sie als separate Variablen zu halten, macht jede Aktualisierung am Aufrufort deutlich:
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const [selectedProductId, setSelectedProductId] =
useState<string | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
Auch die React-Dokumentation vertritt dieselbe Ansicht. Werte, die immer zusammen ändern, können sinnvoll gruppiert werden, während überflüssige, widersprüchliche, duplizierte oder tief verschachtelte Daten reduziert werden sollten. Das Zusammenführen unzusammenhängender Werte hat außerdem einen praktischen Nachteil: Jede Aktualisierung muss das vorherige Objekt kopieren, und wenn dies vergessen wird, werden die anderen Felder stillschweigend überschrieben.
Die wichtige Frage ist also nicht, ob eine Reihe von Werten in einem Objekt untergebracht werden kann. Fast alles ist möglich. Fragen Sie stattdessen:
Bilden diese Werte einen zusammenhängenden Zustand, dessen Übergänge zueinander gehören?
Falls ja, kann ihre Gruppierung den Code klarer machen. Falls nein, verschleiert ein kombiniertes Objekt lediglich, welche Aktualisierung was beeinflusst. Der Abbau von Zuständen bedeutet, doppelte Speicherung von Informationen zu beseitigen – nicht, die Komponente in so wenige Hooks wie möglich zu pressen.
Werte ableiten, ohne die Leistung zu ignorieren
Die Ausnahme eines gefilterten Lists aus dem Zustand führt in der Regel auf Einwände: Wird der Filter dann nicht bei jeder Darstellung ausgeführt? Das geschieht tatsächlich, und für die meisten alltäglichen Transformationen ist das genau der richtige Kompromiss. Das Filtern eines Arrays mittlerer Größe ist kostengünstig, und die Berechnung direkt im Code hält die Komponente einfach, ohne nennenswerte Kosten.
Falls die Profilierung zeigt, dass eine Transformation tatsächlich aufwändig ist – beispielsweise eine große Liste, die gefiltert und sortiert werden muss – können Sie das Ergebnis mit useMemo cachen:
const filteredProducts = useMemo(() => {
return products
.filter((product) => {
const matchesSearch = product.name
.toLowerCase()
.includes(search.toLowerCase());
const matchesCategory =
category === "all" ||
product.category === category;
return matchesSearch && matchesCategory;
})
.sort(compareProducts);
}, [products, search, category, sortBy]);
Achten Sie darauf, was useMemo nicht ändert. filteredProducts ist wieder keine schreibbare Zustandsvariable geworden. Es bleibt weiterhin eine reine Funktion ihrer Eingaben; die Memoisierung entscheidet lediglich, ob React das vorherige Ergebnis bei einer bestimmten Renderung wiederverwenden kann, anstatt es erneut zu berechnen. Dadurch bleiben Korrektheit und Optimierung getrennte Aspekte. Die React-Dokumentation stellt useMemo ausschließlich als Leistungsoptimierung dar und warnt davor, sich auf es für ein korrektes Verhalten zu verlassen, da React gespeicherte Werte ignorieren kann.
Die daraus resultierende Reihenfolge der Abläufe ist:
First make the state model correct.
Then measure.
Then optimize expensive calculations if necessary.
Die Verwendung des Zustands als selbst implementierter Cache ändert diese Reihenfolge. Dadurch entsteht bereits zu Beginn eine zusätzliche Synchronisierungskomplexität, bevor überhaupt nachgewiesen werden konnte, dass die Berechnung langsam ist. Es lohnt sich außerdem, zu überprüfen, ob eine gememorierte Berechnung alle Eingaben in ihrem Abhängigkeitsarray auflistet; im obigen Beispiel wird sortBy aufgeführt, weil davon ausgegangen wird, dass der Sortierkomparator darauf angewiesen ist.
Platzieren Sie jeden Zustandsanteil dort, wo die Entscheidung geteilt wird
Auch Zustände, die tatsächlich vorhanden sein müssen, können Probleme verursachen, wenn sie im falschen Komponenten befinden. Nehmen wir an, jede Produktzeile verfolgt ihre eigene Auswahl:
function ProductRow({ product }) {
const [selected, setSelected] = useState(false);
// ...
}
Das funktioniert solange jede Zeile unabhängig umgeschaltet werden kann. Nun ändert sich die Anforderung: Es darf gleichzeitig nur ein Produkt ausgewählt werden. Plötzlich besitzen mehrere Geschwisterkomponenten jeweils ihre eigene Kopie dessen, was eigentlich eine einzige gemeinsame Information sein sollte – nämlich welches Produkt ausgewählt ist. Wenn diese Antwort für mehrere Geschwisterkomponenten relevant ist, sollte ihr Elternteil sie verwalten:
function ProductTable({ products }) {
const [selectedProductId, setSelectedProductId] =
useState<string | null>(null);
return products.map((product) => (
<ProductRow
key={product.id}
product={product}
selected={product.id === selectedProductId}
onSelect={() => setSelectedProductId(product.id)}
/>
));
}
Die Zeilen speichern die Auswahl überhaupt nicht mehr. Sie erhalten einen Booleschen Wert sowie eine Callback-Funktion, und es gibt genau eine einzige Quelle der Wahrheit:
selectedProductId
In den React-Dokumentationen wird dies als Zuweisung genau einer verantwortlichen Komponente an jeden einzelnen Zustandswert dargestellt. Wenn mehrere Komponenten sich auf dieselben Informationen abstimmen müssen, führt die Übertragung dieser Informationen an ihren nächstgelegenen gemeinsamen Elternteil dazu, dass die Kopien nicht auseinanderdriften.
Nichts davon spricht dafür, alles an die Wurzel der Anwendung zu verlagern. Man sollte klarstellen, dass alle anderen benötigten Daten lokal bleiben müssen. Ob ein Hilfetext sichtbar ist, hat nichts mit Authentifizierungsdaten zu tun, und ein halb eingegebenes Formfeld rechtfertigt selten den Einsatz eines globalen Speichers. Zustände sind am einfachsten zu verwalten, wenn ihr Besitzer mit dem Umfang übereinstimmt, in dem die zugrundeliegende Entscheidung geteilt wird. Wenn man sie zu tief platziert, duplizieren die Komponenten die Wahrheit; wenn man sie zu hoch platziert, beginnen entfernte Teile der Anwendung, auf Änderungen neu zu rendern, die für sie irrelevant sind. Die Findung dieser Grenze ist ein wesentlicher Bestandteil einer guten Zustandsarchitektur, und Rethinking React State: Where Your Data Should Actually Live geht näher auf die Optionen lokal, geteilt, serverseitig und über URLs ein.
Reducer organisieren Übergänge, nicht das Modell
Wenn eine Komponente viele Setter enthält, ist der Übergang zu useReducer ein gängiger nächster Schritt – und oft auch der richtige. Anstelle eines Handlers, der mehrere koordinierte Aufrufe wie diese vornimmt:
setStatus("submitting");
setError(null);
setLastAttempt(Date.now());
beschreibt man das Geschehene als ein einzelnes Ereignis:
dispatch({ type: "submitted" });
Ein Reducer sammelt alle Änderungen an einem Ort, was nützlich ist, wenn mehrere miteinander verbundene Werte gleichzeitig geändert werden. Was er jedoch nicht leisten kann, ist, überflüssige Daten dauerhaft unüberflüssig zu machen. Dieser anfängliche Zustand bleibt weiterhin problematisch:
const initialState = {
search: "",
products: [],
filteredProducts: [],
resultCount: 0,
hasResults: true,
};
Durch das Zusammenfassen doppelter Werte in einem Reducer bleibt das Synchronisierungsproblem bestehen; die Synchronisierungslogik wird lediglich in den Reducer verlagert. Jede Aktion könnte heute alle Kopien korrekt aktualisieren, doch das Modell ermöglicht weiterhin mehrere gespeicherte Versionen derselben Informationen, wodurch eine weitere hinzugefügte Aktion eine davon übersehen könnte.
Ein besserer Reducer behält nur die Eingaben bei:
const initialState = {
search: "",
category: "all",
sortBy: "name",
};
Die sichtbare Produktliste wird anschließend bei der Renderung aus dem Zustand des Reducers zusammen mit den aktuellen products erzeugt. useReducer ist ein gutes Werkzeug, wenn die Übergänge kompliziert werden, ersetzt es aber nicht die grundlegendere Frage, was das Komponente tatsächlich speichern muss. Beantworten Sie diese Frage zuerst, bevor Sie das Werkzeug wählen, das den Zustand verwalten soll.
Checkliste zur Überprüfung des Komponentenzustands
useState macht das Hinzufügen von Zustand nahezu reibungslos, und diese Einfachheit verdeckt die architektonischen Kosten. Jede neue Variable ist ein weiterer Wert, der für sich allein ändern kann. Wenn sie etwas bereits Verfügbares dupliziert, sind nun Regeln erforderlich, um beide Versionen in Einklang zu halten. Eine Duplikate ist noch handhabbar. Fünf davon führen jedoch zu einem Wirrwarr aus Effects und Abhängigkeitslisten, Settern, die weitere Setter auslösen, Reset-Code, veralteten Lesevorgängen, widersprüchlichen Flags sowie Fehlern, die erst nach einer bestimmten Klicksequenz auftauchen. Die Lösung ist selten ein intelligenterer Synchronisierungsmechanismus; meist sollte die Synchronisierung von vornherein nicht existieren.
Wenn der Zustand einer Komponente ständig wächst, gehen Sie jede gespeicherte Wert durch und fragen Sie sich:
- Stellt er eine von dem Benutzer oder dem System getroffene Entscheidung dar?
- Muss die Komponente ihn über mehrere Aufrufe hinweg speichern?
Diese Fragen liefern weitaus mehr Informationen als eine simple Auflistung der Hooks. Eine Komponente, die acht unabhängige, notwendige Zustandswerte enthält, kann durchaus gut gestaltet sein, während eine mit nur drei Variablen bereits zu viele hat, wenn zwei davon Kopien oder Folgen der dritten sind.
Wichtige Erkenntnisse
- Speichern Sie Ursachen wie Benutzereingaben und Auswahlentscheidungen; erzeugen Sie während der Darstellung Folgen wie gefilterte Listen, Zählwerte und Flags.
- Ein Effect, der den Zustand nur aus anderen Zuständen setzt, deutet darauf hin, dass der zweite Wert eigentlich eine Berechnung sein sollte.
- Modellieren Sie gegenseitig ausschließende Modi als einen einzigen Statuswert, damit unmögliche Kombinationen nicht dargestellt werden können.
- Gruppieren Sie Werte nur dann, wenn sie zusammenändern; ein großes Objekt ist an sich kein Ziel.
- wenden Sie
useMemonach der Messung an und bedenken Sie, dass es sich um einen Cache handelt und keine Quelle der Wahrheit ist. - Geben Sie gemeinsamen Fakten einen einzigen Eigentümer im niedrigsten gemeinsamen Elternelement und halten Sie rein lokale UI-Zustände lokal.
- Wenn eine Komponente schwer zu modifizieren wird, fragen Sie vor dem Hinzufügen eines weiteren Setters, welchen realen Faktor jede Zustandsvariable repräsentiert. Der Zustand, den es am einfachsten ist im Einklang zu halten, ist der Zustand, den Sie nie gespeichert haben.
Verwandte Literatur
- React Query und Redux: Die Server-State in großen Anwendungen neu überdenken — Erfahren Sie, warum eine Produktions-Chat-App TanStack Query anstelle von Redux zur Verwaltung von Serverdaten verwendet hat, und wo Redux in der modernen React-Architektur noch seinen Platz findet.
- Die React-State neu überdenken: Wo Ihre Daten tatsächlich gespeichert werden sollten — Dieser Artikel erklärt, wie man React-Fehler verringern kann, indem man den State in die URL, das DOM oder abgeleitete Werte verlegt, anstatt useState übermäßig zu nutzen.