Recoil führte das Atommodell in die React- Welt ein. Die neuen Befugnisse gingen mit einer steilen Lernkurve und spärlichen Lernressourcen einher.
Jotai und Zedux vereinfachten später verschiedene Aspekte dieses neuen Modells, boten viele neue Funktionen und erweiterten die Grenzen dieses erstaunlichen neuen Paradigmas.
Andere Artikel konzentrieren sich auf die Unterschiede zwischen diesen Tools. Dieser Artikel konzentriert sich auf ein großes Merkmal, das alle drei gemeinsam haben:
Sie haben Flux repariert.
Wenn Sie Flux nicht kennen, finden Sie hier eine kurze Zusammenfassung:
Außer Redux folgten grundsätzlich alle Flux-basierten Bibliotheken diesem Muster: Eine App verfügt über mehrere Stores. Es gibt nur einen Disponenten, dessen Aufgabe es ist, die Aktionen in der richtigen Reihenfolge an alle Filialen weiterzuleiten. Diese „richtige Reihenfolge“ bedeutet, dass Abhängigkeiten zwischen den Filialen dynamisch sortiert werden.
Nehmen Sie zum Beispiel die Einrichtung einer E-Commerce-App:
Wenn der Benutzer beispielsweise eine Banane in seinen Warenkorb legt, muss PromosStore warten, bis der Status von CartStore aktualisiert wird, bevor er eine Anfrage sendet, um zu sehen, ob ein Bananencoupon verfügbar ist.
Oder vielleicht können Bananen nicht in den Bereich des Benutzers geliefert werden. Der CartStore muss den UserStore vor der Aktualisierung überprüfen. Oder vielleicht können Gutscheine nur einmal pro Woche verwendet werden. Der PromosStore muss den UserStore überprüfen, bevor er die Gutscheinanfrage sendet.
Flux mag diese Abhängigkeiten nicht. Aus den alten React-Dokumenten :
Die Objekte innerhalb einer Flux-Anwendung sind stark entkoppelt und folgen sehr stark dem Gesetz von Demeter , dem Prinzip, dass jedes Objekt innerhalb eines Systems so wenig wie möglich über die anderen Objekte im System wissen sollte.
Die Theorie dahinter ist solide. 100%. Also ... warum ist diese Multi-Store-Variante von Flux ausgestorben?
Nun stellt sich heraus, dass Abhängigkeiten zwischen isolierten Zustandscontainern unvermeidlich sind. Um den Code modular und trocken zu halten, sollten Sie tatsächlich häufig andere Stores nutzen.
In Flux werden diese Abhängigkeiten spontan erstellt:
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
Dieses Beispiel zeigt, dass Abhängigkeiten nicht direkt zwischen Geschäften deklariert werden, sondern vielmehr pro Aktion zusammengesetzt werden. Um diese informellen Abhängigkeiten zu finden, muss der Implementierungscode durchsucht werden.
Das ist ein sehr einfaches Beispiel! Aber man sieht bereits, wie durcheinander Flux sich anfühlt. Nebenwirkungen, Auswahloperationen und Zustandsaktualisierungen sind alle zusammengeschustert. Diese Colocation kann tatsächlich ganz nett sein. Mischen Sie jedoch einige informelle Abhängigkeiten ein, verdreifachen Sie das Rezept und servieren Sie es auf einem Boilerplate, und Sie werden sehen, dass Flux schnell zusammenbricht.
Andere Flux-Implementierungen wie Flummox und Reflux verbesserten die Boilerplate- und Debugging-Erfahrung. Obwohl es sehr benutzerfreundlich war, war das Abhängigkeitsmanagement das einzige lästige Problem, das alle Flux-Implementierungen plagte. Die Nutzung eines anderen Ladens fühlte sich hässlich an. Tief verschachtelte Abhängigkeitsbäume waren schwer zu verfolgen.
Diese E-Commerce-App könnte eines Tages Stores für OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded usw. haben. Eine große App könnte leicht Hunderte von Stores haben. Wie halten Sie die Abhängigkeiten in jedem Geschäft auf dem neuesten Stand? Wie verfolgen Sie Nebenwirkungen? Wie steht es mit der Reinheit? Was ist mit dem Debuggen? Sind Bananen wirklich eine Beere?
Was die von Flux eingeführten Programmierprinzipien betrifft, war der unidirektionale Datenfluss ein Gewinner, das Gesetz von Demeter jedoch vorerst nicht.
Wir alle wissen, wie Redux herbeieilte, um den Tag zu retten. Das Konzept mehrerer Filialen wurde zugunsten eines Singleton-Modells aufgegeben. Jetzt kann alles ohne jegliche „Abhängigkeiten“ auf alles andere zugreifen.
Reduzierer sind rein, daher muss die gesamte Logik, die sich mit mehreren Zustandsabschnitten befasst, außerhalb des Speichers erfolgen. Die Community hat Standards für den Umgang mit Nebenwirkungen und abgeleiteten Zuständen erstellt. Redux-Stores sind wunderbar debuggbar. Der einzige große Flux-Fehler, den Redux ursprünglich nicht beheben konnte, war sein Boilerplate.
RTK vereinfachte später das berüchtigte Standardbeispiel von Redux. Dann entfernte Zustand etwas Flaum auf Kosten einiger Debugging-Leistungen. Alle diese Tools erfreuen sich in der React-Welt großer Beliebtheit.
Mit dem modularen Zustand werden Abhängigkeitsbäume von Natur aus so komplex, dass die beste Lösung, die wir uns vorstellen konnten, war: „Mach es einfach nicht, schätze ich.“
Und es hat funktioniert! Dieser neue Singleton-Ansatz funktioniert für die meisten Apps immer noch gut genug. Die Flux-Prinzipien waren so solide, dass sie durch einfaches Entfernen des Abhängigkeitsalptraums behoben werden konnten.
Oder doch?
Der Erfolg des Singleton-Ansatzes wirft die Frage auf: Worauf wollte Flux überhaupt hinaus? Warum wollten wir jemals mehrere Geschäfte?
Erlauben Sie mir, etwas Licht ins Dunkel zu bringen.
Bei mehreren Geschäften werden die Staatsstücke in ihre eigenen autonomen, modularen Container aufgeteilt. Diese Geschäfte können isoliert getestet werden. Sie können auch problemlos zwischen Apps und Paketen geteilt werden.
Diese autonomen Speicher können in separate Codeblöcke aufgeteilt werden. In einem Browser können sie zeitverzögert geladen und im Handumdrehen eingebunden werden.
Die Reduzierer von Redux lassen sich auch relativ einfach im Code aufteilen. Dank replaceReducer
besteht der einzige zusätzliche Schritt darin, den neuen kombinierten Reduzierer zu erstellen. Allerdings können weitere Schritte erforderlich sein, wenn Nebenwirkungen und Middleware beteiligt sind.
Beim Singleton-Modell ist es schwierig zu wissen, wie man den internen Zustand eines externen Moduls mit Ihrem eigenen integrieren kann. Die Redux-Community hat das Ducks-Muster eingeführt, um dieses Problem zu lösen. Und es funktioniert, allerdings mit ein wenig Aufwand.
Bei mehreren Stores kann ein externes Modul einfach einen Store freigeben. Beispielsweise kann eine Formularbibliothek einen FormStore exportieren. Dies hat den Vorteil, dass der Standard „offiziell“ ist, was bedeutet, dass es weniger wahrscheinlich ist, dass Menschen ihre eigenen Methoden entwickeln. Dies führt zu einem robusteren, einheitlicheren Community- und Paket-Ökosystem.
Das Singleton-Modell ist überraschend leistungsfähig. Redux hat das bewiesen. Allerdings weist insbesondere das Auswahlmodell eine harte Obergrenze auf. Ich habe in dieser Reselect-Diskussion einige Gedanken dazu geschrieben. Ein großer, teurer Selektorbaum kann sich wirklich in die Länge ziehen, selbst wenn man die maximale Kontrolle über das Caching übernimmt.
Andererseits sind bei mehreren Filialen die meisten Zustandsaktualisierungen auf einen kleinen Teil des Zustandsbaums beschränkt. Sie berühren nichts anderes im System. Dies ist weit über den Singleton-Ansatz hinaus skalierbar. Tatsächlich ist es bei mehreren Speichern sehr schwierig, die CPU-Beschränkungen zu erreichen, bevor die Speicherbeschränkungen auf dem Computer des Benutzers erreicht werden.
In Redux ist es nicht allzu schwierig, den Zustand zu zerstören. Genau wie im Code-Splitting-Beispiel sind nur wenige zusätzliche Schritte erforderlich, um einen Teil der Reduzierhierarchie zu entfernen. Bei mehreren Filialen ist es jedoch noch einfacher: Theoretisch können Sie die Filiale einfach vom Disponenten trennen und die Müllabfuhr zulassen.
Dies ist das große Problem, mit dem Redux, Zustand und das Singleton-Modell im Allgemeinen nicht gut umgehen können. Nebenwirkungen werden von dem Zustand getrennt, mit dem sie interagieren. Die Auswahllogik ist von allem getrennt. Während Flux mit mehreren Filialen möglicherweise zu eng aneinandergereiht war, verfiel Redux in das entgegengesetzte Extrem.
Bei mehreren autonomen Geschäften passen diese Dinge natürlich zusammen. Tatsächlich fehlten Flux nur ein paar Standards, um zu verhindern, dass alles zu einem Wirrwarr von Kauderwelsch wird (sorry).
Wenn Sie nun die OG Flux-Bibliothek kennen, wissen Sie, dass sie überhaupt nicht großartig war. Der Disponent verfolgt immer noch einen globalen Ansatz – er leitet jede Aktion an jede Filiale weiter. Die ganze Sache mit informellen/impliziten Abhängigkeiten machte auch die Aufteilung und Zerstörung des Codes nicht perfekt.
Dennoch hatte Flux viele coole Funktionen zu bieten. Darüber hinaus bietet der Multi-Store-Ansatz Potenzial für noch mehr Funktionen wie Inversion of Control und fraktale (auch lokale) Zustandsverwaltung.
Flux hätte sich vielleicht zu einem wirklich mächtigen Staatsverwalter entwickelt, wenn nicht jemand seine Göttin Demeter genannt hätte. Es ist mein ernst! ... Ok, das bin ich nicht. Aber jetzt, wo Sie es erwähnen, verdient das Demeter-Gesetz vielleicht einen genaueren Blick:
Was genau ist dieses sogenannte „Gesetz“? Aus Wikipedia :
- Jede Einheit sollte nur begrenzte Kenntnisse über andere Einheiten haben: nur Einheiten, die „eng“ mit der aktuellen Einheit verwandt sind.
- Jede Einheit sollte nur mit ihren Freunden sprechen; Sprich nicht mit Fremden.
Dieses Gesetz wurde im Hinblick auf die objektorientierte Programmierung entwickelt, kann jedoch in vielen Bereichen angewendet werden, einschließlich der React-Zustandsverwaltung.
Die Grundidee besteht darin, zu verhindern, dass ein Geschäft:
In Bezug auf Bananen sollte eine Banane keine andere Banane schälen und nicht mit einer Banane in einem anderen Baum sprechen. Es kann jedoch mit dem anderen Baum kommunizieren, wenn die beiden Bäume zuerst eine Bananentelefonleitung aufbauen.
Dies fördert die Trennung von Anliegen und trägt dazu bei, dass Ihr Code modular, trocken und solide bleibt. Solide Theorie! Was hat Flux also übersehen?
Nun, Abhängigkeiten zwischen den Filialen sind ein natürlicher Bestandteil eines guten, modularen Systems. Wenn ein Store eine weitere Abhängigkeit hinzufügen muss, sollte er dies so explizit wie möglich tun . Hier ist noch einmal etwas von diesem Flux-Code:
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore verfügt über mehrere Abhängigkeiten, die auf unterschiedliche Weise deklariert werden – es wartet auf CartStore
und liest von dort und es liest von UserStore
. Die einzige Möglichkeit, diese Abhängigkeiten zu entdecken, besteht darin, in der PromosStore-Implementierung nach Geschäften zu suchen.
Entwicklungstools können auch nicht dazu beitragen, diese Abhängigkeiten besser erkennbar zu machen. Mit anderen Worten: Die Abhängigkeiten sind zu implizit.
Obwohl dies ein sehr einfaches und erfundenes Beispiel ist, veranschaulicht es, wie Flux das Gesetz von Demeter falsch interpretierte. Obwohl ich mir sicher bin, dass es hauptsächlich aus dem Wunsch heraus entstanden ist, die Flux-Implementierungen klein zu halten (echte Abhängigkeitsverwaltung ist eine komplexe Aufgabe!), hat Flux hier jedoch versagt.
Anders als die Helden dieser Geschichte:
Im Jahr 2020 betrat Recoil die Bühne. Auch wenn es anfangs etwas ungeschickt war, haben wir dadurch ein neues Muster kennengelernt, das den Multi-Store-Ansatz von Flux wiederbelebte.
Der unidirektionale Datenfluss wurde vom Speicher selbst zum Abhängigkeitsdiagramm verschoben. Speicher wurden jetzt Atome genannt. Atome waren ordnungsgemäß autonom und im Code aufteilbar. Sie verfügten über neue Kräfte wie Spannungsunterstützung und Flüssigkeitszufuhr. Und am wichtigsten ist, dass Atome ihre Abhängigkeiten offiziell erklären.
Das Atommodell war geboren.
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil hatte mit einer aufgeblähten Codebasis, Speicherlecks, schlechter Leistung, langsamer Entwicklung und instabilen Funktionen zu kämpfen – vor allem mit Nebenwirkungen. Einige davon konnten langsam ausgebügelt werden, aber in der Zwischenzeit übernahmen andere Bibliotheken Recoils Ideen und setzten sie um.
Jotai betrat die Szene und gewann schnell eine Anhängerschaft.
// a Jotai atom const greetingAtom = atom('Hello, World!')
Abgesehen davon, dass es nur einen winzigen Bruchteil der Größe von Recoil ausmacht, bot Jotai aufgrund seines WeakMap-basierten Ansatzes eine bessere Leistung, schlankere APIs und keine Speicherverluste.
Dies ging jedoch auf Kosten einiger Leistung – der WeakMap-Ansatz erschwert die Cache-Kontrolle und macht die gemeinsame Nutzung des Status zwischen mehreren Fenstern oder anderen Bereichen nahezu unmöglich. Und das Fehlen von String-Tasten ist zwar elegant, macht das Debuggen jedoch zu einem Albtraum. Die meisten Apps sollten diese wieder hinzufügen, was die Eleganz von Jotai drastisch beeinträchtigt.
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
Einige lobende Erwähnungen sind Reatom und Nanostores . Diese Bibliotheken haben die Theorie hinter dem Atommodell weiter erforscht und versuchen, seine Größe und Geschwindigkeit bis an die Grenzen auszureizen.
Das Atommodell ist schnell und skaliert sehr gut. Aber bis vor Kurzem gab es ein paar Bedenken, auf die keine Atombibliothek so gut reagiert hatte:
Die Lernkurve. Atome sind unterschiedlich . Wie machen wir diese Konzepte für React-Entwickler zugänglich?
Dev X und Debuggen. Wie machen wir Atome erkennbar? Wie verfolgen Sie Aktualisierungen oder setzen bewährte Praktiken durch?
Inkrementelle Migration für vorhandene Codebasen. Wie greifen Sie auf externe Stores zu? Wie halten Sie die bestehende Logik aufrecht? Wie vermeiden Sie eine vollständige Neufassung?
Plugins. Wie machen wir das Atommodell erweiterbar? Kann es jede mögliche Situation bewältigen?
Abhängigkeitsspritze. Atome definieren natürlicherweise Abhängigkeiten, aber können sie während des Testens oder in anderen Umgebungen ausgetauscht werden?
Das Gesetz der Demeter. Wie verbergen wir Implementierungsdetails und verhindern vereinzelte Updates?
Hier komme ich ins Spiel. Sehen Sie, ich bin der Hauptschöpfer einer weiteren Atombibliothek:
Vor ein paar Wochen betrat Zedux endlich die Bühne. Zedux wurde von einem Fintech-Unternehmen in New York entwickelt – dem Unternehmen, für das ich arbeite – und ist nicht nur darauf ausgelegt, schnell und skalierbar zu sein, sondern auch ein elegantes Entwicklungs- und Debugging-Erlebnis zu bieten.
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
Ich werde hier nicht näher auf die Funktionen von Zedux eingehen – wie gesagt, dieser Artikel konzentriert sich nicht auf die Unterschiede zwischen diesen atomaren Bibliotheken.
Es genügt zu sagen, dass Zedux alle oben genannten Bedenken berücksichtigt. Es ist zum Beispiel die erste atomare Bibliothek, die eine echte Umkehrung der Kontrolle bietet, und die erste, die uns den Kreis zurück zum Gesetz von Demeter schließt, indem sie Atomexporte zum Verbergen von Implementierungsdetails anbietet.
Die letzten Ideologien von Flux wurden endlich wiederbelebt – nicht nur wiederbelebt, sondern verbessert! - dank des Atommodells.
Was genau ist das Atommodell?
Diese atomaren Bibliotheken weisen viele Unterschiede auf – sie haben sogar unterschiedliche Definitionen dessen, was „atomar“ bedeutet. Der allgemeine Konsens besteht darin, dass Atome kleine, isolierte, autonome Zustandscontainer sind, die über einen gerichteten azyklischen Graphen reaktiv aktualisiert werden.
Ich weiß, ich weiß, es klingt komplex, aber warten Sie, bis ich es mit Bananen erkläre.
Ich scherze nur! Eigentlich ist es ganz einfach:
Aktualisierungen prallen durch die Grafik. Das ist es!
Der Punkt ist, dass alle diese atomaren Bibliotheken unabhängig von der Implementierung oder der Semantik das Konzept mehrerer Stores wiederbelebt haben und sie nicht nur nutzbar gemacht haben, sondern auch zu einer echten Freude beim Arbeiten.
Die 6 Gründe, die ich für den Wunsch nach mehreren Geschäften genannt habe, sind genau die Gründe, warum das Atommodell so leistungsfähig ist:
Allein die einfachen APIs und die Skalierbarkeit machen Atombibliotheken zu einer hervorragenden Wahl für jede React-App. Mehr Leistung und weniger Leistung als Redux? Ist das ein Traum?
Was für eine Reise! Die Welt des React-Zustandsmanagements überrascht immer wieder und ich bin so froh, dass ich mitgefahren bin.
Wir fangen gerade erst an. Bei Atomen gibt es viel Raum für Innovation. Nachdem ich jahrelang Zedux erstellt und verwendet habe, habe ich gesehen, wie leistungsfähig das Atommodell sein kann. Tatsächlich ist seine Kraft seine Achillesferse:
Wenn Entwickler Atome erforschen, tauchen sie oft so tief in die Möglichkeiten ein, dass sie zurückkommen und sagen: „Sehen Sie sich diese verrückte komplexe Kraft an“, anstatt: „Schauen Sie sich an, wie einfach und elegant Atome dieses Problem lösen.“ Ich bin hier, um das zu ändern.
Das Atommodell und die Theorie dahinter wurden nicht auf eine Weise vermittelt, die für die meisten React-Entwickler zugänglich ist. In gewisser Weise war die bisherige Erfahrung der React-Welt mit Atomen das Gegenteil von Flux:
Dieser Artikel ist der zweite in einer Reihe von Lernressourcen, die ich erstelle, um React-Entwicklern zu helfen, zu verstehen, wie Atombibliotheken funktionieren und warum Sie möglicherweise eine verwenden möchten. Schauen Sie sich den ersten Artikel an – Scalability: the Lost Level of React State Management .
Es hat 10 Jahre gedauert, aber die von Flux eingeführte solide CS-Theorie hat dank des Atommodells endlich große Auswirkungen auf React-Apps. Und das wird auch in den kommenden Jahren so bleiben.