Redux: Alternativer Ansatz für asynchrone Aktionen

Erstellt am 27. Dez. 2015  ·  44Kommentare  ·  Quelle: reduxjs/redux

Ich habe nach einer Alternative für die Ausführung asynchroner Aktionen in Redux gesucht und würde mich über Kommentare anderer zu meinem Vorgehen freuen.

Um meinen Ansatz zu veranschaulichen, habe ich das asynchrone Beispiel in meinem Klon von Redux geändert: https://github.com/winstonewert/redux/tree/master/examples/async

Normalerweise werden externe Aktionen ausgeführt, indem die Aktionsersteller asynchron gemacht werden. Im Fall des asynchronen Beispiels sendet der Ersteller der fetchPosts-Aktion eine REQUEST_POSTS-Aktion, um den Beginn der Anforderung anzuzeigen, gefolgt von einer RECEIVE_POSTS-Aktion, sobald die Posts von der API zurückgekommen sind.

In meinem Beispiel sind alle Aktionsersteller synchron. Stattdessen gibt es eine Funktion, die die Liste der asynchronen Aktionen zurückgibt, die basierend auf dem Zustand derzeit stattfinden sollten. Siehe mein Beispiel hier: https://github.com/rackt/redux/compare/master...winstonewert :master#diff-8a94dc7aa7bdc6e5390c9216a69761f8R12

Die doReactions-Funktion abonniert den Speicher und stellt sicher, dass der tatsächliche Status der aktuell gestellten Anforderungen mit dem vom doReactions-Status zurückgegebenen Status übereinstimmt, indem Anforderungen gestartet oder abgebrochen werden.

Was ist also der Unterschied?

1) Die Reaktionsfunktion ist eine reine Zustandsfunktion. Dies erleichtert das Testen.
2) Die eigentliche Logik der zu stellenden Anfragen ist einfacher. Sehen Sie sich die wenigen Zeilenfunktionen in meinem Beispiel im Vergleich zu den verschiedenen Logikelementen an, die zuvor durch Container und Aktionsersteller verbreitet wurden.
3) Erleichtert das Stornieren von Anfragen.

Irgendwelche Gedanken?

discussion feedback wanted

Hilfreichster Kommentar

Auch ich habe viel über alternative Wege nachgedacht, mit Nebenwirkungen in Redux umzugehen, und ich hoffe, dass ich Ihren Thread nicht kapere, wenn ich einige der Probleme, die ich bei einigen aktuellen Ansätzen sehe, aus dem Gehirn werfe und warum und wie ich denke, dass dies so ist Trotz seiner scheinbaren Einfachheit ein großer Schritt in die richtige Richtung.

Das Problem mit Nebenwirkungen bei Action Creators

In rein funktionalen Sprachen werden Seiteneffekte immer an den Rand der Anwendung gehoben und zur Ausführung an die Laufzeit zurückgegeben. In Elm Reducer geben Sie ein Tupel zurück, das den neuen Zustand und alle Effekte enthält, die ausgeführt werden sollen. Methoden mit dieser Signatur sind jedoch noch nicht mit anderen Redux-Reduzierern zusammensetzbar.

Der offensichtliche (aber möglicherweise nicht der beste) Ort, um Nebenwirkungen in Redux auszuführen, sind die Aktionsersteller und mehrere verschiedene Middleware-Optionen wurden entwickelt, um dieses Muster zu unterstützen. Ich denke jedoch, dass die aktuellen Middleware-Ansätze eher eine Problemumgehung sind, um Nebenwirkungen als erstklassiges Konzept der Reduzierer nicht zurückzugeben.

Während die Leute immer noch großartige Dinge mit Redux bauen und es ein großer Schritt nach vorne und viel einfacher und pragmatischer als die meisten Alternativen ist, gibt es ein paar Probleme, die ich mit Nebenwirkungen bei Action-Erstellern sehe:

  • Impliziter Zustand ist ausgeblendet
  • Duplizierung der Geschäftslogik
  • Kontextannahmen und/oder Abhängigkeiten reduzieren die Wiederverwendbarkeit
  • Action-Ersteller mit Nebenwirkungen sind schwer zu testen
  • Kann nicht optimiert oder gestapelt werden

Impliziter Zustand ist ausgeblendet

In der Counter-Anwendung IncrementAsync erzeugt ein Timeout und erst nach dessen Abschluss wird der Anwendungsstatus aktualisiert. Wenn Sie beispielsweise einen visuellen Indikator anzeigen möchten, dass eine Inkrementierungsoperation im Gange ist, kann die Ansicht dies nicht aus dem Anwendungsstatus ableiten. Dieser Zustand ist implizit und verborgen.

Obwohl manchmal elegant, bin ich mir bei dem Vorschlag , Generatoren als Orchestratoren von Aktionserstellern zu verwenden, nicht so sicher, da der implizite Zustand verborgen ist und nicht einfach serialisiert werden kann.

Mit redux-thunk oder ähnlichem könnten Sie mehrere Nachrichten an den Reducer senden, die ihn darüber informieren, wenn die Inkrementierungsoperation gestartet und abgeschlossen wurde, aber dies führt zu anderen Problemen.

Das Zurückspulen des Zustands bis zu einem Punkt, an dem die Inkrementierungsoperation als im Gange markiert ist, nachdem der Effekt abgeschlossen ist, wird den Nebeneffekt nicht wirklich regenerieren und bleibt daher auf unbestimmte Zeit im Gange.

Ihr Vorschlag scheint dieses Problem zu lösen. Da Nebenwirkungen aus dem Zustand erzeugt werden, muss die Absicht mit dem resultierenden Zustand in irgendeiner Form ausgedrückt werden Lassen Sie den Staat in der Schwebe.

Duplizierung der Geschäftslogik

Es ist natürlich, dass Aktionen nur dann einen Nebeneffekt erzeugen, wenn sich die Anwendung in einem bestimmten Zustand befindet. Wenn ein Aktionsersteller in Redux einen Zustand erfordert, muss dies eine einfache und reine Funktion sein oder explizit mit dem Zustand versehen sein.

Nehmen wir als einfaches Beispiel an, dass wir mit der Beispiel-Zähleranwendung beginnen und den Zähler jedes Mal in eine zufällige Schriftfarbe ändern möchten, wenn der Zähler ein Vielfaches von 5 ist.

Da die Generierung von Zufallszahlen unrein ist, wird dieses Verhalten im Aktionsersteller vorgeschlagen. Es gibt jedoch mehrere verschiedene Aktionen, die den Wert des Zählers, Inkrement, Dekrement, InkrementAsync, InkrementIfOdd (das in diesem Fall nicht geändert werden muss) ändern können.

Inkrementieren und Dekrementieren erforderten bisher keinen Zustand, da sie zuvor im Reducer behandelt wurden und somit Zugriff auf den aktuellen Wert hatten, aber da ein Reducer keine Nebenwirkungen haben oder zurückgeben kann (Zufallszahlengenerierung), werden diese Funktionen jetzt zu unreinen Aktionserstellern, die benötigen um den aktuellen Zählerwert zu kennen, um zu bestimmen, ob es notwendig ist, eine neue zufällige Schriftfarbe auszuwählen, und diese Logik muss in allen Erstellern von Gegenaktionen dupliziert werden.

Eine mögliche Alternative zum expliziten Bereitstellen des aktuellen Zustands wäre, Redux-Thunk zu verwenden und einen Rückruf zurückzugeben, um auf den aktuellen Zustand zuzugreifen. Dadurch können Sie vermeiden, dass alle Orte geändert werden, an denen Aktionen erstellt werden, um den aktuellen Wert bereitzustellen, aber der Aktionsersteller muss jetzt wissen, wo im globalen Anwendungsstatus der Wert gespeichert ist, und dies schränkt die Möglichkeit ein, denselben Zähler innerhalb von . mehrmals wiederzuverwenden der gleichen Anwendung oder in verschiedenen Anwendungen, bei denen der Status unterschiedlich strukturiert sein kann.

Kontextannahmen und/oder Abhängigkeiten reduzieren die Wiederverwendbarkeit

Wenn Sie sich das Gegenbeispiel noch einmal ansehen, werden Sie feststellen, dass es nur eine Gegeninstanz gibt. Obwohl es trivial ist, viele Zähler auf der Seite zu haben, die denselben Status anzeigen/aktualisieren, sind zusätzliche Änderungen am Zähler erforderlich, wenn jeder Zähler einen anderen Status verwenden soll.

Dies wurde zuvor besprochen. Wie erstellt man eine generische Liste als Reduzierer und Komponenten-Enhancer?

Wenn der Zähler nur einfache Aktionstypen verwenden würde, wäre es relativ trivial, die Ulmen-Architektur anzuwenden.

In diesem Fall umschließt das übergeordnete Element einfach die Aktionsersteller oder den Dispatcher, um die Nachricht mit dem erforderlichen Kontext zu ergänzen, und kann dann den Reduzierer direkt mit dem lokalisierten Zustand aufrufen.

Während das React Elmish-Beispiel beeindruckend erscheint, fehlen in dem Beispiel insbesondere die beiden problematischen Aktionsersteller InkrementIfOdd und InkrementAsync.

InkrementIfOdd hängt von Middleware ab, um den aktuellen Status zu bestimmen, und muss daher seine Position innerhalb des Anwendungsstatus kennen.

InkrementAsync löst schließlich direkt eine Inkrementierungsaktion aus, die nicht für die übergeordnete Komponente verfügbar gemacht wird und daher nicht mit zusätzlichem Kontext umschlossen werden kann.

Ihr Vorschlag behebt dieses Problem zwar nicht direkt, aber wenn InkrementAsync als einfache Aktion implementiert wurde, die den Zustand in {counter: 0, incrementAfterDelay: 1000} geändert hat, um den Nebeneffekt in einem Store-Listener auszulösen, wird InkrementAsync zu einer einfachen Nachricht. InkrementIfOdd ist rein, könnte also entweder im Reducer implementiert werden oder der Zustand explizit bereitgestellt werden.... Dadurch wird es möglich, die elm-Architektur auf Wunsch wieder anzuwenden.

Action-Ersteller mit Nebenwirkungen sind schwer zu testen

Ich denke, es ist ziemlich offensichtlich, dass Nebenwirkungen schwieriger zu testen sein werden. Sobald Ihre Nebenwirkungen vom aktuellen Zustand und der Geschäftslogik abhängig sind, werden sie nicht nur schwieriger, sondern auch wichtiger zu testen.

Ihr Vorschlag ermöglicht es einem, auf einfache Weise einen Test zu erstellen, der zeigt, dass ein Zustandsübergang einen Zustand erzeugt, der die gewünschten Reaktionen enthält, ohne tatsächlich eine davon auszuführen. Reaktionen sind auch einfacher zu testen, da sie keinen bedingten Zustand oder keine Geschäftslogik benötigen.

Kann nicht optimiert oder gestapelt werden

In einem kürzlich erschienenen Blogbeitrag von John A De Goes wurde das Problem mit undurchsichtigen Datentypen wie IO oder Task zum Ausdrücken von Effekten diskutiert. Durch die Verwendung deklarativer Beschreibungen von Nebenwirkungen anstelle von undurchsichtigen Typen haben Sie das Potenzial, Effekte später zu optimieren oder zu kombinieren.

Eine moderne Architektur für FP

Thunks, Promises und Generatoren sind undurchsichtig und daher müssen Optimierungen wie Batching und/oder das Unterdrücken doppelter API-Aufrufe explizit mit ähnlichen Funktionen wie fetchPostsIfNeeded gehandhabt werden.

Ihr Vorschlag eliminiert fetchPostsIfNeeded und es scheint durchaus machbar, eine reactions Funktion zu implementieren, die mehrere Anfragen optimieren und/oder je nach Bedarf verschiedene APIs verwenden könnte, wenn mehr oder weniger Daten angefordert wurden.

Meine Umsetzung

Ich habe vor kurzem einen Redux-Fork erstellt, der es einem ermöglicht, Reducer zu erstellen, die nur den neuen Zustand zurückgeben, wie sie es jetzt tun, oder ein spezielles Objekt mit Effekten, das den neuen Zustand und eine Beschreibung aller Effekte enthält, die nach dem Reducer ausgeführt werden sollen.

Ich war mir nicht sicher, wie dies ohne Forking-Redux erfolgen sollte, da Compose- und CombineReducers modifiziert werden mussten, um die Auswirkungen auf vorhandene Reducer aufzuheben, um die Kompatibilität mit bestehendem Reducer-Code aufrechtzuerhalten.

Ihr Vorschlag ist jedoch insofern ganz nett, als er keine Änderung von Redux erfordert. Darüber hinaus denke ich, dass Ihre Lösung das implizite Problem des versteckten Zustands besser löst und die resultierenden Reaktionen wahrscheinlich einfacher zu kombinieren oder zu optimieren ist.

Zusammenfassung

So wie React "nur die Benutzeroberfläche" ist und nicht sehr vorschreibt, wie man den Anwendungsstatus tatsächlich speichert oder aktualisiert, ist Redux meistens "nur der Speicher" und gibt nicht sehr vor, wie man mit Nebenwirkungen umgeht.

Ich kann niemandem vorwerfen, pragmatisch zu sein und Dinge zu erledigen, und die vielen Mitwirkenden an Redux und der Middleware haben es den Leuten ermöglicht, wirklich coole Sachen schneller und besser zu bauen, als es zuvor möglich war. Nur durch ihre Beiträge sind wir so weit gekommen. Daher ein besonderer Dank an alle, die dazu beigetragen haben.

Redux ist toll. Dies sind keine notwendigen Probleme mit Redux selbst, aber hoffentlich konstruktive Kritik an den aktuellen Architekturmustern und den Beweggründen und potenziellen Vorteilen für das Ausführen von Effekten nach und nicht vor Zustandsänderungen.

Alle 44 Kommentare

Interessanter Ansatz! Es scheint, als ob es idealerweise von der Art der Asynchronität entkoppelt werden sollte, wie der Methode, die zum Ausführen des XHR verwendet wird, oder sogar, dass eine Webanforderung die Quelle der Asynchronität ist.

Auch ich habe viel über alternative Wege nachgedacht, mit Nebenwirkungen in Redux umzugehen, und ich hoffe, dass ich Ihren Thread nicht kapere, wenn ich einige der Probleme, die ich bei einigen aktuellen Ansätzen sehe, aus dem Gehirn werfe und warum und wie ich denke, dass dies so ist Trotz seiner scheinbaren Einfachheit ein großer Schritt in die richtige Richtung.

Das Problem mit Nebenwirkungen bei Action Creators

In rein funktionalen Sprachen werden Seiteneffekte immer an den Rand der Anwendung gehoben und zur Ausführung an die Laufzeit zurückgegeben. In Elm Reducer geben Sie ein Tupel zurück, das den neuen Zustand und alle Effekte enthält, die ausgeführt werden sollen. Methoden mit dieser Signatur sind jedoch noch nicht mit anderen Redux-Reduzierern zusammensetzbar.

Der offensichtliche (aber möglicherweise nicht der beste) Ort, um Nebenwirkungen in Redux auszuführen, sind die Aktionsersteller und mehrere verschiedene Middleware-Optionen wurden entwickelt, um dieses Muster zu unterstützen. Ich denke jedoch, dass die aktuellen Middleware-Ansätze eher eine Problemumgehung sind, um Nebenwirkungen als erstklassiges Konzept der Reduzierer nicht zurückzugeben.

Während die Leute immer noch großartige Dinge mit Redux bauen und es ein großer Schritt nach vorne und viel einfacher und pragmatischer als die meisten Alternativen ist, gibt es ein paar Probleme, die ich mit Nebenwirkungen bei Action-Erstellern sehe:

  • Impliziter Zustand ist ausgeblendet
  • Duplizierung der Geschäftslogik
  • Kontextannahmen und/oder Abhängigkeiten reduzieren die Wiederverwendbarkeit
  • Action-Ersteller mit Nebenwirkungen sind schwer zu testen
  • Kann nicht optimiert oder gestapelt werden

Impliziter Zustand ist ausgeblendet

In der Counter-Anwendung IncrementAsync erzeugt ein Timeout und erst nach dessen Abschluss wird der Anwendungsstatus aktualisiert. Wenn Sie beispielsweise einen visuellen Indikator anzeigen möchten, dass eine Inkrementierungsoperation im Gange ist, kann die Ansicht dies nicht aus dem Anwendungsstatus ableiten. Dieser Zustand ist implizit und verborgen.

Obwohl manchmal elegant, bin ich mir bei dem Vorschlag , Generatoren als Orchestratoren von Aktionserstellern zu verwenden, nicht so sicher, da der implizite Zustand verborgen ist und nicht einfach serialisiert werden kann.

Mit redux-thunk oder ähnlichem könnten Sie mehrere Nachrichten an den Reducer senden, die ihn darüber informieren, wenn die Inkrementierungsoperation gestartet und abgeschlossen wurde, aber dies führt zu anderen Problemen.

Das Zurückspulen des Zustands bis zu einem Punkt, an dem die Inkrementierungsoperation als im Gange markiert ist, nachdem der Effekt abgeschlossen ist, wird den Nebeneffekt nicht wirklich regenerieren und bleibt daher auf unbestimmte Zeit im Gange.

Ihr Vorschlag scheint dieses Problem zu lösen. Da Nebenwirkungen aus dem Zustand erzeugt werden, muss die Absicht mit dem resultierenden Zustand in irgendeiner Form ausgedrückt werden Lassen Sie den Staat in der Schwebe.

Duplizierung der Geschäftslogik

Es ist natürlich, dass Aktionen nur dann einen Nebeneffekt erzeugen, wenn sich die Anwendung in einem bestimmten Zustand befindet. Wenn ein Aktionsersteller in Redux einen Zustand erfordert, muss dies eine einfache und reine Funktion sein oder explizit mit dem Zustand versehen sein.

Nehmen wir als einfaches Beispiel an, dass wir mit der Beispiel-Zähleranwendung beginnen und den Zähler jedes Mal in eine zufällige Schriftfarbe ändern möchten, wenn der Zähler ein Vielfaches von 5 ist.

Da die Generierung von Zufallszahlen unrein ist, wird dieses Verhalten im Aktionsersteller vorgeschlagen. Es gibt jedoch mehrere verschiedene Aktionen, die den Wert des Zählers, Inkrement, Dekrement, InkrementAsync, InkrementIfOdd (das in diesem Fall nicht geändert werden muss) ändern können.

Inkrementieren und Dekrementieren erforderten bisher keinen Zustand, da sie zuvor im Reducer behandelt wurden und somit Zugriff auf den aktuellen Wert hatten, aber da ein Reducer keine Nebenwirkungen haben oder zurückgeben kann (Zufallszahlengenerierung), werden diese Funktionen jetzt zu unreinen Aktionserstellern, die benötigen um den aktuellen Zählerwert zu kennen, um zu bestimmen, ob es notwendig ist, eine neue zufällige Schriftfarbe auszuwählen, und diese Logik muss in allen Erstellern von Gegenaktionen dupliziert werden.

Eine mögliche Alternative zum expliziten Bereitstellen des aktuellen Zustands wäre, Redux-Thunk zu verwenden und einen Rückruf zurückzugeben, um auf den aktuellen Zustand zuzugreifen. Dadurch können Sie vermeiden, dass alle Orte geändert werden, an denen Aktionen erstellt werden, um den aktuellen Wert bereitzustellen, aber der Aktionsersteller muss jetzt wissen, wo im globalen Anwendungsstatus der Wert gespeichert ist, und dies schränkt die Möglichkeit ein, denselben Zähler innerhalb von . mehrmals wiederzuverwenden der gleichen Anwendung oder in verschiedenen Anwendungen, bei denen der Status unterschiedlich strukturiert sein kann.

Kontextannahmen und/oder Abhängigkeiten reduzieren die Wiederverwendbarkeit

Wenn Sie sich das Gegenbeispiel noch einmal ansehen, werden Sie feststellen, dass es nur eine Gegeninstanz gibt. Obwohl es trivial ist, viele Zähler auf der Seite zu haben, die denselben Status anzeigen/aktualisieren, sind zusätzliche Änderungen am Zähler erforderlich, wenn jeder Zähler einen anderen Status verwenden soll.

Dies wurde zuvor besprochen. Wie erstellt man eine generische Liste als Reduzierer und Komponenten-Enhancer?

Wenn der Zähler nur einfache Aktionstypen verwenden würde, wäre es relativ trivial, die Ulmen-Architektur anzuwenden.

In diesem Fall umschließt das übergeordnete Element einfach die Aktionsersteller oder den Dispatcher, um die Nachricht mit dem erforderlichen Kontext zu ergänzen, und kann dann den Reduzierer direkt mit dem lokalisierten Zustand aufrufen.

Während das React Elmish-Beispiel beeindruckend erscheint, fehlen in dem Beispiel insbesondere die beiden problematischen Aktionsersteller InkrementIfOdd und InkrementAsync.

InkrementIfOdd hängt von Middleware ab, um den aktuellen Status zu bestimmen, und muss daher seine Position innerhalb des Anwendungsstatus kennen.

InkrementAsync löst schließlich direkt eine Inkrementierungsaktion aus, die nicht für die übergeordnete Komponente verfügbar gemacht wird und daher nicht mit zusätzlichem Kontext umschlossen werden kann.

Ihr Vorschlag behebt dieses Problem zwar nicht direkt, aber wenn InkrementAsync als einfache Aktion implementiert wurde, die den Zustand in {counter: 0, incrementAfterDelay: 1000} geändert hat, um den Nebeneffekt in einem Store-Listener auszulösen, wird InkrementAsync zu einer einfachen Nachricht. InkrementIfOdd ist rein, könnte also entweder im Reducer implementiert werden oder der Zustand explizit bereitgestellt werden.... Dadurch wird es möglich, die elm-Architektur auf Wunsch wieder anzuwenden.

Action-Ersteller mit Nebenwirkungen sind schwer zu testen

Ich denke, es ist ziemlich offensichtlich, dass Nebenwirkungen schwieriger zu testen sein werden. Sobald Ihre Nebenwirkungen vom aktuellen Zustand und der Geschäftslogik abhängig sind, werden sie nicht nur schwieriger, sondern auch wichtiger zu testen.

Ihr Vorschlag ermöglicht es einem, auf einfache Weise einen Test zu erstellen, der zeigt, dass ein Zustandsübergang einen Zustand erzeugt, der die gewünschten Reaktionen enthält, ohne tatsächlich eine davon auszuführen. Reaktionen sind auch einfacher zu testen, da sie keinen bedingten Zustand oder keine Geschäftslogik benötigen.

Kann nicht optimiert oder gestapelt werden

In einem kürzlich erschienenen Blogbeitrag von John A De Goes wurde das Problem mit undurchsichtigen Datentypen wie IO oder Task zum Ausdrücken von Effekten diskutiert. Durch die Verwendung deklarativer Beschreibungen von Nebenwirkungen anstelle von undurchsichtigen Typen haben Sie das Potenzial, Effekte später zu optimieren oder zu kombinieren.

Eine moderne Architektur für FP

Thunks, Promises und Generatoren sind undurchsichtig und daher müssen Optimierungen wie Batching und/oder das Unterdrücken doppelter API-Aufrufe explizit mit ähnlichen Funktionen wie fetchPostsIfNeeded gehandhabt werden.

Ihr Vorschlag eliminiert fetchPostsIfNeeded und es scheint durchaus machbar, eine reactions Funktion zu implementieren, die mehrere Anfragen optimieren und/oder je nach Bedarf verschiedene APIs verwenden könnte, wenn mehr oder weniger Daten angefordert wurden.

Meine Umsetzung

Ich habe vor kurzem einen Redux-Fork erstellt, der es einem ermöglicht, Reducer zu erstellen, die nur den neuen Zustand zurückgeben, wie sie es jetzt tun, oder ein spezielles Objekt mit Effekten, das den neuen Zustand und eine Beschreibung aller Effekte enthält, die nach dem Reducer ausgeführt werden sollen.

Ich war mir nicht sicher, wie dies ohne Forking-Redux erfolgen sollte, da Compose- und CombineReducers modifiziert werden mussten, um die Auswirkungen auf vorhandene Reducer aufzuheben, um die Kompatibilität mit bestehendem Reducer-Code aufrechtzuerhalten.

Ihr Vorschlag ist jedoch insofern ganz nett, als er keine Änderung von Redux erfordert. Darüber hinaus denke ich, dass Ihre Lösung das implizite Problem des versteckten Zustands besser löst und die resultierenden Reaktionen wahrscheinlich einfacher zu kombinieren oder zu optimieren ist.

Zusammenfassung

So wie React "nur die Benutzeroberfläche" ist und nicht sehr vorschreibt, wie man den Anwendungsstatus tatsächlich speichert oder aktualisiert, ist Redux meistens "nur der Speicher" und gibt nicht sehr vor, wie man mit Nebenwirkungen umgeht.

Ich kann niemandem vorwerfen, pragmatisch zu sein und Dinge zu erledigen, und die vielen Mitwirkenden an Redux und der Middleware haben es den Leuten ermöglicht, wirklich coole Sachen schneller und besser zu bauen, als es zuvor möglich war. Nur durch ihre Beiträge sind wir so weit gekommen. Daher ein besonderer Dank an alle, die dazu beigetragen haben.

Redux ist toll. Dies sind keine notwendigen Probleme mit Redux selbst, aber hoffentlich konstruktive Kritik an den aktuellen Architekturmustern und den Beweggründen und potenziellen Vorteilen für das Ausführen von Effekten nach und nicht vor Zustandsänderungen.

Ich versuche, den Unterschied zwischen diesem Ansatz und Redux-Saga zu verstehen. Ich interessiere mich für die Behauptung, dass es den Zustand in Generatoren implizit verbirgt, weil es zunächst so aussieht, als würde es dasselbe tun. Aber ich nehme an, das hängt davon ab, wie io.take implementiert ist. Wenn die Saga eine Aktion nur verarbeitet, wenn sie gerade bei diesem yield blockiert ist, dann verstehe ich definitiv, was Sie meinen. Aber wenn redux-saga Aktionen in die Warteschlange stellt, so dass io.take vergangene Aktionen zurückgibt, scheint es dasselbe zu tun. In jedem Fall haben Sie eine Logik, die dispatch Aktionen asynchron ausführen kann, ausgelöst durch das Abhören des Aktionsstreams.

Es ist jedoch ein interessantes Konzept. Konzeptualisieren von Redux als Action-Stream, aus dem Zustandsübergänge und Effekte ausgelöst werden. Das scheint mir eine alternative Sichtweise zu sein, als nur einen staatlichen Prozessor zu betrachten.

Beim Event-Sourcing-Modell kommt es meiner Meinung nach darauf an, ob Redux-Aktionen "Befehle" (bedingte Aufforderungen zum Ausführen einer Aktion) oder "Ereignisse" (atomare Zustandsübergänge, die sich in einer flachen Ansicht widerspiegeln) sind. Ich denke, wir haben ein Werkzeug, das flexibel genug ist, um in beide Richtungen gedacht zu werden.

Auch ich bin mit dem Status Quo der "Smart Action Creators" etwas unzufrieden, aber ich gehe das anders an, wobei Redux eher der Event-Store ist -- wo Aktionen eine von vielen möglichen Effekten sind, die könnte von einer externen "Controller" -Schicht ausgelöst werden. Ich habe Code, der diesem Ansatz folgte, in React-Redux-Controller integriert , obwohl ich eine unausgegorene Idee über einen möglicherweise leichteren Weg habe, dies zu erreichen. Es würde jedoch einen Hook erfordern, den es derzeit nicht gibt, und einige Store-Wrap-Hijinks, die ich noch nicht ganz verstanden habe.

Beschriebene Hijinks speichern https://github.com/rackt/redux/issues/1200

Ich versuche, den Unterschied zwischen diesem Ansatz und Redux-Saga zu verstehen

Ich habe Redux-Saga erst gesehen, als ich meinen Ansatz gefunden hatte, aber es gibt definitiv einige Ähnlichkeiten. Aber ich habe noch einige Unterschiede:

  1. Mein Ansatz hat keinen Zugriff auf den Action-Stream, sondern nur auf den Zustand. redux-saga kann den Prozess einfach starten, weil eine Aktion stattgefunden hat. Mein Ansatz erfordert, dass ein Reduzierer den Zustand ändert, der die Reaktionsfunktion auslöst, um die Aktion anzufordern.
  2. Mein Ansatz erfordert, dass alle Zustände im Zustand von Redux vorhanden sind. Redux-saga hat den zusätzlichen Zustand, der im Saga-Generator lebt (auf welcher Zeile es sich befindet, die Werte lokaler Variablen).
  3. Mein Ansatz isoliert den asynchronen Teil. Die tatsächliche Logik der Reaktion kann getestet werden, ohne sich mit der asynchronen Funktionalität zu befassen. Die Saga fügt diese zusammen.
  4. Die Saga vereint verschiedene Teile derselben Logik. Mein Ansatz zwingt Sie dazu, eine Saga in Teile aufzuteilen, die in die Implementierung von Reduzierer, Reaktionen und Reaktionstyp gehören.

Grundsätzlich betont mein Ansatz reine Funktionen und alles im Redux-Zustand zu halten. Der redux-saga-Ansatz betont, dass er ausdrucksstärker ist. Ich denke, es gibt Vor- und Nachteile, aber meins gefällt mir besser. Aber ich bin voreingenommen.

Das klingt wirklich vielversprechend. Ich denke, es wäre zwingender, ein Beispiel zu sehen, das die Reaktionsmaschinerie von der Domänenlogik trennt.

Ihr Vorschlag eliminiert fetchPostsIfNeeded und es scheint durchaus machbar, eine Reaktionsfunktion zu implementieren, die mehrere Anfragen optimieren und/oder je nach Bedarf verschiedene APIs verwenden könnte, wenn mehr oder weniger Daten angefordert wurden.

So wie es aussieht, konnte man das in der Reaktionsfunktion nicht wirklich machen. Die Logik dort müsste wissen, welche Aktionen bereits gestartet wurden (wir können nichts mehr hineingeben), aber die Reaktionsfunktion hat die Informationen nicht. Die Reaktionsmaschinerie, die die Funktion responses() verbraucht, könnte diese Dinge sicherlich tun.

Ich denke, es wäre zwingender, ein Beispiel zu sehen, das die Reaktionsmaschinerie von der Domänenlogik trennt.

Ich nehme an, Sie meinen, wie die Funktion doReactions() das Starten/Beenden des XMLHttpRequest behandelt? Ich habe verschiedene Wege erforscht, dies zu tun. Das Problem ist, dass es schwierig ist, einen generischen Weg zu finden, um festzustellen, ob zwei Reaktionen tatsächlich dieselbe Reaktion sind. isEqual von Lodash funktioniert fast, scheitert aber bei Schließungen.

Ich nehme an, Sie meinen, wie die Funktion doReactions() das Starten/Beenden des XMLHttpRequest behandelt?

Nein, ich meine nur, dass in Ihrem Beispiel die gesamte Konfiguration zum Einrichten des Konzepts einer Reaktion mit der Domänenlogik des Abrufens von Daten sowie den Details zum Abrufen dieser Daten vermischt wird. Es scheint mir, dass die generischen Aspekte in etwas ausgegliedert werden sollten, das weniger an die spezifischen Details des Beispiels gekoppelt ist.

Nein, ich meine nur, dass in Ihrem Beispiel die gesamte Konfiguration zum Einrichten des Konzepts einer Reaktion mit der Domänenlogik des Abrufens von Daten sowie den Details zum Abrufen dieser Daten vermischt wird. Es scheint mir, dass die generischen Aspekte in etwas ausgegliedert werden sollten, das weniger an die spezifischen Details des Beispiels gekoppelt ist.

Hmm... Ich denke, wir meinen mit Domänenlogik nicht dasselbe.

Aus meiner Sicht kapselt die Funktion responses() die Domänenlogik und ist von der Funktion doReactions() getrennt, die die Logik der Anwendung von Reaktionen behandelt. Aber du meinst anscheinend was anderes...

So wie es aussieht, konnte man das in der Reaktionsfunktion nicht wirklich machen. Die Logik dort müsste wissen, welche Aktionen bereits gestartet wurden (wir können nichts mehr hineingeben), aber die Reaktionsfunktion hat die Informationen nicht. Die Reaktionsmaschinerie, die die Funktion responses() verbraucht, könnte diese Dinge sicherlich tun.

Ich meinte hauptsächlich, dass, wenn ein einzelnes Ereignis eine Zustandsänderung auslöst, bei der mehrere Komponenten die gleichen Informationen anfordern, es möglicherweise in der Lage ist, sie zu optimieren. Sie haben jedoch Recht, dass es allein nicht ausreicht, festzustellen, ob eine Nebenwirkung einer vorherigen Zustandsänderung noch ansteht und somit die zusätzliche Anfrage unnötig ist.

Ich dachte anfangs, dass man vielleicht alle Zustände im App-Zustand behalten könnte, aber als ich anfing, über das aktuelle Stoppuhr-Problem nachzudenken isOn zwar im Anwendungszustand gespeichert werden sollte, die tatsächliche interval Objekt, das dieser Stoppuhr zugeordnet ist, muss woanders gespeichert werden. isOn sollte sich im App-Zustand befinden, ist aber in diesem Fall nicht allein ausreichend.

Ich meinte hauptsächlich, dass, wenn ein einzelnes Ereignis eine Zustandsänderung auslöst, bei der mehrere Komponenten die gleichen Informationen anfordern, es möglicherweise in der Lage ist, sie zu optimieren. Sie haben jedoch Recht, dass es allein nicht ausreicht, festzustellen, ob eine Nebenwirkung einer vorherigen Zustandsänderung noch ansteht und somit die zusätzliche Anfrage unnötig ist.

Ich dachte daran, Anfragen zusammenzuführen oder zu stapeln. Das Eliminieren von Duplikaten sollte problemlos funktionieren. Eigentlich sollte es auch den Fall von anstehenden Zustandsänderungen gut handhaben, da sie immer noch von der Reaktionsfunktion zurückgegeben (und somit dedupuliert) werden, bis die Serverantwort zurückkommt.

Ich dachte anfangs, dass man vielleicht alle Zustände im App-Zustand behalten könnte, aber als ich anfing, über das aktuelle Stoppuhr-Problem nachzudenken, wurde mir klar, dass die Tatsache, dass die Stoppuhr eingeschaltet ist, im Anwendungszustand gespeichert werden sollte, das eigentliche Intervallobjekt, das dieser Stoppuhr zugeordnet ist muss woanders gespeichert werden. isOn sollte sich im App-Zustand befinden, ist aber in diesem Fall nicht allein ausreichender Zustand.

So wie ich es betrachte, sind die aktuell anstehenden Reaktionen wie deine Reaktionskomponenten. Technisch gesehen haben sie einen internen Zustand, aber wir modellieren sie als Funktion des aktuellen Zustands.

Hmm... Ich denke, wir meinen mit Domänenlogik nicht dasselbe.

Aus meiner Sicht kapselt die Funktion responses() die Domänenlogik und ist von der Funktion doReactions() getrennt, die die Logik der Anwendung von Reaktionen behandelt. Aber du meinst anscheinend was anderes...

Ich habe das ganze /reactions/index Modul als Ganzes genommen, aber ja, ich würde zustimmen, dass die reactions Funktion reine Domänenlogik ist. Aber anstatt sich in einem domänenspezifischen Modul zu befinden, ist es zusammen mit dem Boilerplate doReactions verpackt. Das soll Ihre Methodik nicht umhauen, es macht es nur schwieriger, die Trennung zwischen Bibliothekscode und App-Code auf einen Blick zu verstehen.

Dann scheint mir doReactions selbst ziemlich eng mit einer bestimmten Methode des bestimmten Vorgangs des Abrufens von Daten von einer API verbunden zu sein. Ich würde vermuten, dass eine ausgefeilte Reaktionsbibliothek eine Möglichkeit sein könnte, Handler für verschiedene Arten von Effekten zu registrieren.

Das soll Ihre Methode nicht umhauen; Diese Vorgehensweise finde ich sehr ansprechend.

Ich bin mir nicht sicher, ob der Zustand der Reaktionskomponente eine gute Analogie ist, da die meisten Reaktionszustände
sollte sich im App-Zustand befinden, aber es muss anscheinend einen Weg geben
um den Zustand zwischen Dispatch-Ereignissen aufrechtzuerhalten, die nicht in die
Geschäft.

Ich denke, diese Art von Zustand bezeichnet @yelouafi als Kontrollzustand und
vielleicht ist sagas ein guter Weg, um den nicht-serialisierbaren Zustand des zu modellieren
System als unabhängiger Beobachter/Akteur.

Ich denke, ich wäre weniger besorgt über den versteckten Saga-Zustand, wenn Sagas
reagierte nur auf anwendungsgenerierte Ereignisse (Reaktionen) statt auf Benutzer
initiierte Ereignisse (Aktionen), da dies dem App-Reducer ermöglichen würde, die
aktuellen Zustand und eine beliebige bedingte Geschäftslogik, um zu bestimmen, ob die
Anwendung soll den gewünschten Nebeneffekt ohne Duplizierung ermöglichen
Geschäftslogik.
Am Montag, den 4. Januar 2016 um 17:56 Uhr Winston Ewert [email protected]
schrieb:

Ich meinte meistens, wenn ein einzelnes Ereignis eine Zustandsänderung auslöste, in der
mehrere Komponenten die gleichen Informationen angefordert haben, dann ist es möglicherweise in der Lage
optimieren sie. Sie haben jedoch Recht, dass es nicht ausreicht, um
feststellen, ob eine Nebenwirkung einer vorherigen Zustandsänderung noch ansteht
und somit entfällt die zusätzliche Anfrage.

Ich dachte daran, Anfragen zusammenzuführen oder zu stapeln. Eliminieren von Duplikaten
sollte ganz gut funktionieren. Eigentlich sollte es den Fall des schwebenden Zustands behandeln
ändert sich auch gut, da sie immer noch von der
Reaktionen funktionieren (und somit dedupuliert) bis die Serverantwort
kommt zurück.

Ich dachte anfangs, vielleicht könnte man alle Zustände in der App behalten
Staat, aber als ich anfing, über das aktuelle Stoppuhr-Problem nachzudenken,
erkannte, dass die Tatsache, dass die Stoppuhr eingeschaltet ist, in
der Anwendungszustand das dazugehörige eigentliche Intervallobjekt
Stoppuhr muss woanders aufbewahrt werden. isOn sollte in der App sein
Zustand, aber ist nicht allein ist in diesem Fall kein ausreichender Zustand.

So wie ich das sehe, sind die aktuell anstehenden Reaktionen wie deine
Komponenten reagieren. Technisch gesehen haben sie einen gewissen internen Zustand, aber wir modellieren
sie in Abhängigkeit vom aktuellen Zustand.


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/rackt/redux/issues/1182#issuecomment -168858051.

Das soll Ihre Methodik nicht umhauen, es macht es nur schwieriger, die Trennung zwischen Bibliothekscode und App-Code auf einen Blick zu verstehen.

Das ist völlig gerecht.

Dann scheint mir doReactions selbst ziemlich eng mit einer bestimmten Methode des bestimmten Vorgangs des Abrufens von Daten von einer API verbunden zu sein. Ich würde vermuten, dass eine ausgefeilte Reaktionsbibliothek eine Möglichkeit sein könnte, Handler für verschiedene Arten von Effekten zu registrieren.

Ja. Ich versuche immer noch herauszufinden, wie ich es am besten aufteilen kann. Es wird durch das Problem der Gleichheitsprüfung kompliziert.

Ich bin mir nicht sicher, ob der Zustand der Reaktionskomponente eine gute Analogie ist, da die meisten Reaktionszustände
sollte sich im App-Zustand befinden, aber es muss anscheinend einen Weg geben
um den Zustand zwischen Dispatch-Ereignissen aufrechtzuerhalten, die nicht in die
Geschäft.

Entschuldigung, ich glaube, ich habe die Analogie vermasselt. Mein Punkt ist nicht, den Zustand der externen Aktion mit dem Zustand der Komponente zu vergleichen, sondern den Zustand des DOM. Das Intervall oder XMLHttpRequest sind eher wie die DOM-Elemente, die reagieren, erstellen und zerstören. Sie sagen einfach reagieren, was das aktuelle DOM sein soll und machen es möglich. Ebenso geben Sie einfach den Satz der aktuellen externen Reaktionen zurück, und das Framework bricht die Aktion ab oder startet sie, um sie wahr zu machen.

Diese Herangehensweise finde ich auch sehr interessant. Haben Sie überlegt, mehrere doReactions , die unterschiedliche Zustandszuordnungen benötigen? Ich denke, es wäre ähnlich wie bei cyclejs, wo Sie wiederverwendbare Treiber erstellen können:

function main(action$) {
  const state$ = action$.startWith(INITIAL_STATE).scan(reducer);

  return { 
    DOM: state$.map(describeDOM),
    HTTP: state$.map(describeRequests),
    ...
  };
}

Ein Unterschied besteht darin, dass Sie die Treiber nicht nach Ereignissen abfragen, um den Aktionsstream zu erhalten ( const someEvent$ = sources.DOM.select('.class').events('click') ), sondern die Aktionen direkt in der Senke angeben ( <button onClick={() => dispatch(action())} /> ) wie bei HTTP-Anfragen sowie.

Ich denke, die React-Analogie funktioniert ziemlich gut. Ich würde das DOM jedoch nicht als den internen Zustand betrachten, sondern als die API, mit der es arbeitet, während der interne Zustand aus den Komponenteninstanzen und dem virtuellen Dom besteht.

Hier ist eine Idee für die API (mit React; HTTP könnte auch so gebaut werden):

// usage
const describe = (state, dispatch) => <MyComponent state={state} dispatch={dispatch} />;
const driver = createReactDOMDriver({ container } /* opts */);
store.subscribe(() => driver.update(describe(store.getState(), store.dispatch)); 
// (could be simplified further to eg. `store.use(driver, describe)` )

// implementation
const createReactDOMDriver = ({ container }) => {
  return {
    update: (element) => ReactDOM.render(element, container),
    destroy: () => ReactDOM.unmountComponentAtNode(container),
  };
};

Ich würde describe getState (anstelle eines Zustandsschnappschusses) und dispatch . Auf diese Weise könnte es so asynchron sein, wie es sein möchte.

Haben Sie überlegt, mehrere doReactions zu verwenden, die unterschiedliche Zustandszuordnungen benötigen?

Ich hatte kurz darüber nachgedacht, und ich gehe jetzt ein bisschen hin und her. Es macht es natürlich, verschiedene Reaktionsbibliotheken zu haben, die verschiedene Dinge tun, eine für das DOM, eine für http, eine für Timer, eine für Web-Audio usw. Jede kann die Optimierungen/das Verhalten entsprechend ihrem eigenen Fall durchführen. Es scheint jedoch weniger hilfreich zu sein, wenn Sie eine App haben, die eine Reihe einmaliger externer Aktionen ausführt.

Ich würde die Beschreibung nehmen getState (anstelle eines Zustands-Snapshots) und Dispatch. Auf diese Weise könnte es so asynchron sein, wie es sein möchte.

Ich würde nicht. Meiner Ansicht nach möchten wir die Asynchronität nach Möglichkeit einschränken und keine zusätzlichen Verwendungsmöglichkeiten bereitstellen. Alles, wofür Sie getState() aufrufen möchten, sollte in der Reducer- oder Reaction-Funktion erfolgen. (Aber das ist meine puristische Denkweise, und vielleicht gibt es einen pragmatischen Grund, ihr nicht zu folgen.)

Gutes Argument. Ich habe die Zuordnung zwischen Ihrer Idee und dem Beispiel von @taurose noch nicht ganz durchdacht. Ich nahm hastig an, dass describe die Funktion reactions , aber das stimmt vielleicht nicht.

Aber ja, ich stimme zu, dass die Begrenzung der Asynchronität ideal ist, denn wenn ich die Stoßrichtung Ihrer Idee verstehe, möchten wir, dass Fortsetzungen rein sind und 1:1 mit bestimmten Aspekten im Zustand abgebildet werden, wie z. B. das Vorhandensein eines Array-Mitglieds, das die Absicht beschreibt, dass ein bestimmter Effekt ist im Gange. Auf diese Weise spielt es keine Rolle, ob sie mehrmals ausgeführt werden, und es gibt keinen versteckten Aspekt, dass ein Prozess irgendwo mitten im Fluss angehalten wird, von dem andere Prozesse implizit abhängen könnten.

Ich würde die Beschreibung nehmen getState (anstelle eines Zustands-Snapshots) und Dispatch. Auf diese Weise könnte es so asynchron sein, wie es sein möchte.

describe wird bei jeder Zustandsänderung aufgerufen, also sehe ich keine Notwendigkeit dafür. Es bedeutet nicht, dass es nicht asynchron sein kann. Betrachten Sie die Reaction-Komponenten: Sie würden getState innerhalb Ihrer Rendermethoden oder Event-Handler aufrufen, um den aktuellen Zustand abzurufen, sondern ihn aus den Requisiten lesen.

Aber Sie haben Recht, es kann (sollte) nichts asynchrones tun; Es sollte dies dem Treiber überlassen und ihm einfach einige zugeordnete Zustände und/oder Rückrufe übergeben.

angenommen, beschreiben war die Reaktionsfunktion, aber das kann nicht wahr sein.

Soweit ich das beurteilen kann, ist es ziemlich gleich. Ein Unterschied wäre, dass reactions nicht dispatch bekommt. Während also describe Callbacks zurückgibt, die Aktionen erstellen und versenden, gibt reactions Aktionsersteller zurück.

@winstonewert es ist ein langer Thread und ich habe gerade keine Zeit, um zu lesen oder deinen Code zu überprüfen, aber vielleicht kann @yelouafi dir antworten.

Das Redux-Saga-Projekt ist aus langen Diskussionen hier entstanden

Ich verwende das Saga-Konzept auch seit über einem Jahr für eine Produktions-App, und die Implementierung ist weniger ausdrucksstark, basiert aber nicht auf Generatoren. Hier sind einige Pseudo-Beispiele, die ich für das Konzept für Redux gegeben habe:

Die Umsetzung hier ist alles andere als perfekt, aber es gibt nur eine Idee.

@yelouafi ist sich der Probleme bewusst, die mit der Verwendung von Generatoren verbunden sind, die den Zustand außerhalb von Redux verstecken, und dass es kompliziert ist, eine Saga auf einem Backend zu starten und diesen versteckten Zustand an das Frontend für universelle Apps zu übertragen (falls wirklich benötigt?)

Die Redux-Saga ist zu Redux-Thunk wie Free zu IO-Monad. Die Effekte sind deklarativ und werden derzeit nicht ausgeführt, können introspektiert werden und werden in einem Interpreter ausgeführt (den Sie in Zukunft anpassen können)

Ich verstehe Ihren Standpunkt zu versteckten Zuständen in Generatoren. Aber ist der Redux Store tatsächlich die

Imho ist es konzeptionell keine schlechte Idee, das Saga-Konzept mit Reducer zu entwickeln, und ich stimme Ihnen zu, es ist eine Entscheidung.
Persönlich nach mehr als 1 Jahr Einsatz von Sagen in der Produktion kann ich mich an keinen Anwendungsfall erinnern, bei dem es nützlich gewesen wäre, den Zustand einer Saga aufzunehmen und später wiederherzustellen, daher bevorzuge ich die Aussagekraft von Generatoren, selbst wenn ich diese verliere Merkmal.

Ich hoffe, dass nichts, was ich sage, als Angriff auf die Redux-Saga rübergekommen ist. Ich habe nur darüber gesprochen, wie es sich von dem Ansatz unterschied, den ich mir ausgedacht hatte.

Ich verstehe Ihren Standpunkt zu versteckten Zuständen in Generatoren. Aber ist der Redux Store tatsächlich die wahre Quelle der Wahrheit einer Redux-App? Ich glaube nicht. Redux zeichnet Aktionen auf und gibt sie wieder. Sie können diese Aktionen jederzeit wiederholen, um den Shop neu zu erstellen. Der Redux Store ist wie eine CQRS-Abfrageansicht des Ereignisprotokolls. Dies bedeutet nicht, dass es die einzige Projektion dieses Ereignisprotokolls sein muss. Sie können das gleiche Ereignisprotokoll in verschiedene Abfrageansichten projizieren und in Sagas darauf warten, die ihren Zustand mit Generatoren, globalen veränderlichen Objekten oder Reduzierungen unabhängig von der Technologie verwalten können.

Ich verstehe deinen Punkt hier nicht wirklich. Sie scheinen zu argumentieren, dass eine Saga eine Projektion des Ereignisprotokolls ist? Aber es ist nicht. Wenn ich die Aktionen wiederhole, komme ich nicht an dieselbe Stelle in den Sagen, wenn die Sagen von asynchronen Ereignissen abhängen. Es scheint mir unausweichlich, dass Sagas Zustände erzeugen, die weder im Zustandsspeicher von Redux noch eine Projektion des Ereignisprotokolls sind.

Soweit ich das beurteilen kann, ist es ziemlich gleich. Ein Unterschied wäre, dass Reaktionen nicht versendet werden. Während also describe Callbacks zurückgibt, die Aktionen erstellen und versenden, gibt Reaktionen Aktionsersteller zurück.

Einverstanden. Im Prinzip könnte React die gleiche Schnittstelle verwenden, alle Eventhandler würden einen Action-Creator nehmen, der ausgelöst wird, wenn das Event ausgelöst wird.

Je mehr ich darüber nachdenke, denke ich, dass es viele Synergien zwischen diesem Ansatz und Sagen geben könnte. Ich stimme den vier Punkten von @winstonewert voll und ganz zu. Ich denke, es ist eine gute Sache, dass Reaktionen keine vom Benutzer initiierten Aktionen sehen können, da dies versteckte Zustände verhindert und sicherstellt, dass die Geschäftslogik in Reduzierern nicht in Aktionserstellern oder Sagen dupliziert werden muss. Ich habe jedoch festgestellt, dass Nebeneffekte oft nicht serialisierbare Zustände erzeugen, die nicht im Reaktionsspeicher, Intervalle, Dom-Objekte, http-Anfragen usw. gespeichert werden können. Sagas, rxjs, baconjs usw. sind perfekt für diesen externen nicht serialisierbaren Kontrollzustand.

doReactions könnte durch eine Saga ersetzt werden und die Ereignisquelle für Sagen sollten Reaktionen und nicht Aktionen sein.

Ich hoffe, dass nichts, was ich sage, als Angriff auf Redux-Saga rübergekommen ist

Keineswegs. Ich habe die Diskussion verfolgt, wollte aber keinen Kommentar abgeben, ohne sich Ihren Code genauer anzuschauen.

Auf den ersten Blick. Anscheinend reagieren Sie nur auf Zustandsänderungen. Wie gesagt, es war ein kurzer Blick. Aber es scheint, dass es die Implementierung komplexer Abläufe noch schwieriger macht als der Ulmen-Ansatz (wo Sie sowohl den Staat als auch die Aktion übernehmen). das bedeutet, dass Sie noch mehr Kontrollzustände im Store speichern müssen (wobei App-Zustandsänderungen allein nicht ausreichen, um die entsprechenden Reaktionen abzuleiten)

Klar, nichts kann reine Funktionen schlagen. Ich denke, Reduzierer sind großartig, um Zustandsübergänge auszudrücken, werden aber wirklich seltsam, wenn man sie in Zustandsautomaten umwandelt.

das bedeutet, dass Sie noch mehr Kontrollzustände im Store speichern müssen (wobei App-Zustandsänderungen allein nicht ausreichen, um die entsprechenden Reaktionen abzuleiten)

Ja. Dies scheint mir der entscheidende Differenzierungsaspekt dieses Ansatzes zu sein. Aber ich frage mich, ob dieses Thema in der Praxis transparent gemacht werden könnte, wenn verschiedene Effekttypen in verschiedene "Treiber" verpackt werden können? Ich stelle mir vor, dass es für die Leute ziemlich einfach ist, einfach die gewünschten Treiber auszuwählen oder ihre eigenen für neuartige Effekte zu schreiben.

Ich habe jedoch festgestellt, dass Nebeneffekte oft nicht serialisierbare Zustände erzeugen, die nicht im Reaktionsspeicher, Intervalle, Dom-Objekte, http-Anfragen usw. gespeichert werden können. Sagas, rxjs, baconjs usw. sind perfekt für diesen externen nicht serialisierbaren Kontrollzustand.

Ich sehe noch nicht, was du bist.

Ich denke, Reduzierer sind großartig, um Zustandsübergänge auszudrücken, werden aber wirklich seltsam, wenn man sie in Zustandsautomaten umwandelt.

Ich stimme zu. Wenn Sie eine komplexe Zustandsmaschine von Hand schreiben, haben wir ein Problem. (Eigentlich wäre es nett, wenn wir einen Generator in einen Reduzierer umwandeln könnten).

Aber ich frage mich, ob dieses Thema in der Praxis transparent gemacht werden könnte, wenn verschiedene Effekttypen in verschiedene "Treiber" verpackt werden können? Ich stelle mir vor, dass es für die Leute ziemlich einfach ist, einfach die gewünschten Treiber auszuwählen oder ihre eigenen für neuartige Effekte zu schreiben.

Ich bin mir nicht sicher, was Sie hier denken. Ich kann verschiedene Fahrer sehen, die verschiedene nützliche Dinge tun, aber den Kontrollzustand beseitigen?

@winstonewert nein ich nehme nichts als Angriff. Ich hatte nicht einmal Zeit, mir deinen Code wirklich anzuschauen :)

Ich verstehe deinen Punkt hier nicht wirklich. Sie scheinen zu argumentieren, dass eine Saga eine Projektion des Ereignisprotokolls ist? Aber es ist nicht. Wenn ich die Aktionen wiederhole, komme ich nicht an dieselbe Stelle in den Sagen, wenn die Sagen von asynchronen Ereignissen abhängen. Es scheint mir unausweichlich, dass Sagas Zustände erzeugen, die weder im Zustandsspeicher von Redux noch eine Projektion des Ereignisprotokolls sind.

Nein, bin ich nicht, der Redux Store ist eine Projektion, aber die Saga ist ein einfacher alter einfacher Hörer.

Die Saga (auch Prozessmanager genannt) ist kein neues Konzept, sie stammt aus der CQRS-Welt und war in der Vergangenheit auf Backend-Systemen weit verbreitet.

Die Saga ist keine Projektion eines Ereignisprotokolls auf eine Datenstruktur, es ist ein Stück Orchestrierung, das auf das, was in Ihrem System passiert, hören und Reaktionen ausgeben kann, der Rest sind Implementierungsdetails. Im Allgemeinen hören Sagas auf ein Ereignisprotokoll (und vielleicht andere externe Dinge, wie Zeit...) und können neue Befehle/Ereignisse erzeugen. Auch wenn Sie Ereignisse in Backend-Systemen wiedergeben, deaktivieren Sie im Allgemeinen durch Sagas ausgelöste Nebeneffekte.

Ein Unterschied besteht darin, dass die Saga in Backend-Systemen oft wirklich eine Projektion des Ereignisprotokolls ist: Um seinen Zustand zu ändern, muss es Ereignisse ausgeben und selbst auf diese hören. In Redux-Saga, wie es derzeit implementiert ist, wäre es schwieriger, das Ereignisprotokoll erneut abzuspielen, um den Saga-Zustand wiederherzustellen.

Ich bin mir nicht sicher, was Sie hier denken. Ich kann verschiedene Fahrer sehen, die verschiedene nützliche Dinge tun, aber den Kontrollzustand beseitigen?

Nein, es wird nicht beseitigt, sondern für die meisten Zwecke nur zu einem Implementierungsproblem unter der Haube gemacht.

Es scheint mir, dass es in der Redux-Community einen wirklich starken Konsens gibt, dass das Speichern des Domänenstatus im Store ein großer Gewinn ist (warum sollten Sie sonst überhaupt Redux verwenden?). Etwas weniger ist der Konsens, dass das Speichern des UI-Zustands ein Gewinn ist, im Gegensatz dazu, ihn in Komponenten gekapselt zu haben. Dann gibt es die Idee, den Browserstatus im Store zu synchronisieren, wie die URL (redux-simple-router) oder Formulardaten. Aber dies scheint die letzte Grenze zu sein, um den Status/die Phase eines lang andauernden Prozesses im Geschäft zu speichern.

Es tut mir leid, wenn dies eine Tangente ist, aber ich denke, ein sehr allgemeiner Ansatz mit guter Benutzerfreundlichkeit für Entwickler müsste die folgenden Funktionen haben:

  • Machen Sie es so, dass sich der typische Benutzer nicht wirklich darum kümmern muss, wie Effekte im Geschäft dargestellt werden. Sie sollten mit einfachen APIs interagieren, die diese Details abstrahieren.
  • Machen Sie es so, dass Effekte leicht komponierbar sind. Es sollte sich natürlich anfühlen, Dinge wie Kontrollfluss und Effekte zu tun, die von anderen Effekten abhängig sind. Dies ist natürlich der Punkt, an dem eine Generator-Abstraktion wirklich glänzt. Es spielt gut mit den meisten Kontrollflüssen, wobei Closures eine bemerkenswerte Ausnahme sind. Aber es ist leicht zu erkennen, wie kompliziert asynchrone Abläufe in Redux-Saga oder React-Redux-Controller ausgedrückt werden können.
  • Machen Sie es so, dass der Effektstatus bei Bedarf anderen Ladenkunden leicht zugänglich gemacht werden kann. Auf diese Weise können Sie dem Benutzer beispielsweise den Status eines Multieffektprozesses präsentieren.
  • Vielleicht ist dies offensichtlich, aber jedes Subsystem, das einen Zustand kapselt, synchronisiert diesen Zustand mit Redux, indem es Aktionen absetzt.

Für diesen zweiten Punkt denke ich, dass es etwas sein müsste, das Redux-Saga ziemlich ähnlich ist. Es könnte mit seinen call Wrappern ziemlich nahe an das herankommen, was ich mir vorgestellt habe. Aber eine Saga müsste gewissermaßen "fast-forwardable" sein, damit Sie sie in einen Zwischenzustand deserialisieren können.

Das ist alles eine ziemlich große Aufgabe, aber praktisch gesehen denke ich, dass dies der Fall ist, wenn es große Gewinne gibt, wenn man einen zentralen, serialisierbaren Aktionsdatensatz hat und den Zustand einer gesamten App auf einer sehr granularen Ebene verfolgt Weise, es zu nutzen. Und ich denke, es kann tatsächlich große Gewinne geben. Ich stelle mir eine viel einfachere Möglichkeit vor, Apps mit Benutzer- und Leistungsanalysen zu instrumentieren. Ich stelle mir eine wirklich erstaunliche Testbarkeit vor, bei der verschiedene Subsysteme nur über den Zustand gekoppelt sind.

Ich bin jetzt vielleicht völlig vom Kurs abgekommen, also belasse ich es dabei :)

@acjay Ich denke, wir stimmen dir in diesen Punkten zu, das Problem besteht darin, diese Implementierung zu finden, die all diese korrekt löst :)

Aber es scheint schwierig zu sein, sowohl eine ausdrucksstarke API mit Generatoren als auch die Möglichkeit zu haben, Zeit zu reisen und den Status von Snapshots/Wiederherstellungen zu erstellen... Vielleicht wäre es möglich, die Ausführung des Effekts zu merken, damit wir den Generatorstatus leicht wiederherstellen können...

Ich bin mir nicht sicher, aber dies könnte Sagen im Stil von while(true) { ... } . Wäre Looping nur eine Folge der staatlichen Progression?

@acjay @slorber

Wie ich in (https://github.com/yelouafi/redux-saga/issues/22#issuecomment-168872101) erklärt habe, ist Zeitreisen allein (dh ohne Hot Reload) für Sagen möglich. Alles, was Sie brauchen, um eine Saga zu einem bestimmten Punkt zu bringen, ist die Abfolge der Effekte, die von Anfang bis zu diesem Punkt erzielt wurden, sowie deren Ergebnis (auflösen oder ablehnen). Dann treibst du einfach den Generator mit dieser Sequenz an

Im eigentlichen Master-Branch (noch nicht auf npm freigegeben). Sagas unterstützen die Überwachung, sie senden alle erzielten Effekte sowie deren Ergebnisse als Aktionen an den Laden; sie stellen auch Hierarchieinformationen bereit, um das Kontrollflussdiagramm zu verfolgen.

Dieses Effektprotokoll kann ausgenutzt werden, um eine Saga bis zu einem bestimmten Punkt abzuspielen: Es sind keine echten API-Aufrufe erforderlich, da das Protokoll bereits die vergangenen Antworten enthält.

In den Repo-Beispielen gibt es ein Beispiel für einen Saga-Monitor (implementiert als Redux-Middleware). Es hört auf das Effektprotokoll und behält eine interne Baumstruktur bei (gut gebaut, träge). Sie können eine Ablaufverfolgung des Ablaufs drucken, indem Sie eine Aktion {type: 'LOG_EFFECT'} an das Geschäft senden

Hier ist eine Aufnahme eines Effektprotokolls aus dem asynchronen Beispiel

saga-log-async

Edit: Entschuldigung, fester Bildlink

Faszinierend! Und das Image der Entwicklungstools ist _super_.

Das ist cool :)

Tatsächlich ist dieser Saga-Monitor ziemlich cool.

Wenn ich darüber nachdenke, scheint mir, dass Saga zwei Probleme löst. Erstens behandelt es die asynchronen Effekte. Zweitens handhabt es komplexe Zustandsinteraktionen, die ansonsten eine widerliche handgeschriebene Zustandsmaschine in einem Reduzierer erfordert hätten.

Mein Ansatz geht nur das erste Problem an. Ich habe keine Notwendigkeit für das zweite Problem gefunden. Wahrscheinlich habe ich noch nicht genug Redux-Code geschrieben, um darauf zu laufen.

Ja, aber ich frage mich, ob es eine Möglichkeit gibt, die beiden Ideen zu verschmelzen. Der call Wrapper von redux-saga ist eine ziemlich einfache Indirektionsebene für einen Effekt, aber angenommen, Sie könnten die Middleware mit Treibern für verschiedene Arten von Effekten initialisieren, könnten Sie diese Effekte als JSON-fähige Daten darstellen, die von der Funktion entkoppelt sind das heißt eigentlich. Der Treiber würde die Details des Versands der zugrunde liegenden Zustandsänderungen an das Geschäft handhaben.

Das kann eine ganze Menge zusätzlicher Komplexität für wenig praktischen Nutzen bedeuten. Aber versuchen Sie einfach, dieser Denkweise bis zum Ende zu folgen.

Ok, ich habe mehr von einer Bibliothek zusammengestellt und das reale Beispiel portiert, um es zu verwenden:

Zuerst haben wir die Implementierung von Reaktionen:
https://github.com/winstonewert/redux-reactions/blob/master/src/index.js
Die Schnittstelle hat drei Funktionen: startReactions übernimmt den Speicher, eine Reaktionsfunktion und eine Zuordnung von Namen zu den Treibern. fromEmitter und fromPromiseFactory erstellen beide Treiber.

Hier ruft das Beispiel startReactions auf, um das System zu aktivieren:
https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/store/configureStore.dev.js#L28

Die Grundkonfiguration von Reaktionen ist hier:
https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/index.js.
Die Reaktionsfunktion durchläuft eigentlich nur die Komponenten, die auf Routerinstanzen reagieren, und sucht nach solchen mit einer Reaktion()-Funktion, um die tatsächlich benötigten Reaktionen für diese Seite herauszufinden.

Die Implementierung des Github-API-Reaktionstyps ist hier: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/api.js. Dies ist hauptsächlich das Kopieren/Einfügen von der Middleware, die im ursprünglichen Beispiel verwendet wurde. Der kritische Punkt ist hier: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/api.js#L79 , wo es fromPromiseFactory verwendet, um den Treiber aus einer Funktion zu erstellen, die gibt Versprechen zurück.

Eine komponentenspezifische Reaktionsfunktion finden Sie hier: https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/containers/RepoPage.js#L80.

Die Reaktionsersteller und die gemeinsame Logik befinden sich in https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/data.js

Hallo Leute! Raise hat gerade einen Shop-Enhancer veröffentlicht, mit dem Sie auch ein Elm-Architektur-ähnliches Effektsystem verwenden können! Ich hoffe, dass wir all diese Ansätze in Zukunft lernen und verbessern können, um alle Bedürfnisse der Community zu erfüllen :smile:

https://github.com/raisemarketplace/redux-loop

Jeder, der an der Diskussion interessiert ist, möchte vielleicht hier weitere Diskussionen zu meiner Idee sehen: https://github.com/winstonewert/redux-reactions/issues/7

Du kannst dir hier auch eine Filiale anschauen, in der ich die Counter-App nach meinem Muster überarbeitet habe, um elmischer zu sein:
https://github.com/winstonewert/redux-reactions/tree/elmish/examples/counter

Ich habe auch festgestellt, dass ich den hier verwendeten Ansatz neu erfinde: https://github.com/ccorcos/elmish

Hey @yelouafi , könntest du den Link zur Saga-Monitor-Idee erneut posten? Das sind wirklich tolle Sachen! Der Link scheint tot zu sein(404). Ich würde gerne mehr sehen!

(Ich glaube, das hängt zusammen. Entschuldigung, wenn das der falsche Ort ist)

Könnten wir möglicherweise alle Effekte wie DOM-Rendering behandeln?

  1. jQuery ist ein DOM-Treiber mit imperativer Schnittstelle. React ist ein DOM-Treiber mit deklarativer Schnittstelle. Anstatt zu befehlen: "deaktiviere diese Schaltfläche", erklären wir: "wir brauchen diese Schaltfläche deaktiviert" und der Treiber entscheidet, welche DOM-Manipulationen zu tun sind. Anstatt zu bestellen: " GET \product\123 ", erklären wir: "wir brauchen diese Daten" und der Fahrer entscheidet, welche Anfragen gesendet/storniert werden sollen.
  2. Wir verwenden React-Komponenten als API-to-DOM-Treiber. Lassen Sie uns sie auch als Schnittstelle zu anderen Treibern verwenden.

    • <button ...> - wir bauen unseren View-Layer aus "normalen" React-Komponenten auf

    • <Map ...> - Wir verwenden "Wrapper"-Komponenten, um die zwingende Schnittstelle einer Bibliothek in eine deklarative zu verwandeln. Wir verwenden sie wie "normale" Komponenten, sind aber intern eigentlich Treiber.

    • <Chart ...> - Dies kann je nach Implementierung eine der oben genannten Optionen sein. Die Grenze zwischen "normalen" Komponenten und Treibern ist also schon verwischt.

    • <Http url={'/product/'+props.selectedProductId} onSuccess={props.PRODUCT_LOADED} /> (oder "smart" <Service...> ) - wir bauen unsere Serviceschicht aus (UI-losen) Treiberkomponenten auf

Sowohl View- als auch Service-Layer werden über React-Komponenten beschrieben. Und unsere (verbundenen) Komponenten auf höchster Ebene kleben sie zusammen.
Auf diese Weise bleiben unsere Reduzierer rein und wir führen keine neuen Mittel zur Behandlung von Effekten ein.

Ich bin mir nicht sicher, wie new Date oder Math.random hier reinpassen.

Ist es immer möglich, eine zwingende API in eine deklarative umzuwandeln?
Halten Sie dies überhaupt für eine praktikable Ansicht?

Danke

Angesichts der Tatsache, dass wir über Sagas und andere großartige Tools für asynchrone Aktionen verfügen, können wir dies meiner Meinung nach jetzt sicher abschließen. Schauen Sie sich #1528 an, um einige interessante neue Richtungen zu finden (auch über die Asynchronität hinaus).

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen