Redux: Empfehlungen für Best Practices in Bezug auf Aktionsersteller, Reduzierer und Selektoren

Erstellt am 22. Dez. 2015  ·  106Kommentare  ·  Quelle: reduxjs/redux

Mein Team verwendet Redux nun seit einigen Monaten. Auf dem Weg dorthin habe ich gelegentlich über ein Feature nachgedacht und mich gefragt, "gehört das in einen Action-Creator oder einen Reducer?". Die Dokumentation scheint diesbezüglich etwas vage zu sein. (Oder vielleicht habe ich nur übersehen, wo es behandelt wird, in diesem Fall entschuldige ich mich.) Aber als ich mehr Code und mehr Tests geschrieben habe, habe ich stärkere Meinungen darüber, wo die Dinge _sollten_, und ich dachte, es würde sich lohnen mit anderen teilen und diskutieren.

Hier also meine Gedanken.

Verwenden Sie Selektoren überall

Dieser erste ist nicht streng mit Redux verwandt, aber ich werde ihn trotzdem teilen, da er unten indirekt erwähnt wird. Mein Team verwendet rackt/reselect . Normalerweise definieren wir eine Datei, die Selektoren für einen bestimmten Knoten unseres Zustandsbaums exportiert (zB MyPageSelectors). Unsere "intelligenten" Container verwenden dann diese Selektoren, um unsere "dummen" Komponenten zu parametrisieren.

Im Laufe der Zeit haben wir erkannt, dass die Verwendung derselben Selektoren an anderen Stellen (nicht nur im Zusammenhang mit der Neuauswahl) zusätzliche Vorteile bietet. Wir verwenden sie beispielsweise in automatisierten Tests. Wir verwenden sie auch in Thunks, die von

Daher lautet meine erste Empfehlung, gemeinsame Selektoren _überall_ zu verwenden, auch wenn synchron auf Daten zugegriffen wird (z. B. bevorzuge myValueSelector(state) gegenüber state.myValue ). Dies reduziert die Wahrscheinlichkeit von falsch eingegebenen Variablen, die zu subtilen undefinierten Werten führen, es vereinfacht Änderungen an der Struktur Ihres Geschäfts usw.

Mach _mehr_ in Action-Creators und _weniger_ in Reducern

Ich denke, das ist sehr wichtig, auch wenn es nicht sofort offensichtlich ist. Geschäftslogik gehört in Aktionsmacher. Reduzierstücke sollten dumm und einfach sein. In vielen Einzelfällen spielt es keine Rolle – aber Beständigkeit ist gut und deshalb ist es am besten, dies _konsequent_ zu tun. Dafür gibt es mehrere Gründe:

  1. Aktionsersteller können durch die Verwendung von Middleware wie redux-thunk asynchron sein. Da Ihre Anwendung häufig asynchrone Aktualisierungen Ihres Shops erfordert, wird eine gewisse "Geschäftslogik" in Ihren Aktionen enden.
  2. Aktionsersteller (genauer die Thunks, die sie zurückgeben) können gemeinsame Selektoren verwenden, da sie Zugriff auf den vollständigen Zustand haben. Reduzierer können dies nicht, da sie nur Zugriff auf ihren Knoten haben.
  3. Mit redux-thunk kann ein einzelner Aktionsersteller mehrere Aktionen ausführen, was komplizierte Statusaktualisierungen einfacher macht und eine bessere Wiederverwendung von Code fördert.

Stellen Sie sich vor, Ihr Bundesstaat verfügt über Metadaten zu einer Liste von Elementen. Jedes Mal, wenn ein Element geändert, hinzugefügt oder aus der Liste entfernt wird, müssen die Metadaten aktualisiert werden. Die "Geschäftslogik", um die Liste und ihre Metadaten synchron zu halten, könnte an einigen Stellen leben:

  1. Bei den Reduzierstücken. Jeder Reducer (hinzufügen, bearbeiten, entfernen) ist für die Aktualisierung der Liste _sowie_ der Metadaten verantwortlich.
  2. In den Ansichten (Container/Komponente). Jede Ansicht, die eine Aktion (Hinzufügen, Bearbeiten, Entfernen) aufruft, ist auch für das Aufrufen einer updateMetadata Aktion verantwortlich. Dieser Ansatz ist aus (hoffentlich) offensichtlichen Gründen schrecklich.
  3. Bei den Aktionsmachern. Jeder Aktionsersteller (Hinzufügen, Bearbeiten, Entfernen) gibt einen Thunk zurück , der eine Aktion zum Aktualisieren der Liste und dann eine weitere Aktion zum Aktualisieren der Metadaten auslöst.

Angesichts der oben genannten Auswahlmöglichkeiten ist Option 3 deutlich besser. Beide Optionen 1 und 3 unterstützen die gemeinsame Nutzung von sauberem Code, aber nur Option 3 unterstützt den Fall, dass Listen- und/oder Metadatenaktualisierungen asynchron sein können. (Zum Beispiel hängt es vielleicht von einem Webworker ab.)

Schreiben Sie "Enten"-Tests, die sich auf Aktionen und Selektoren konzentrieren

Die effizienteste Methode zum Testen von Aktionen, Reduzierern und Selektoren besteht darin, beim Schreiben von Tests dem "Enten" -Ansatz zu folgen. Das bedeutet, dass Sie einen Testsatz schreiben sollten, der einen bestimmten Satz von Aktionen, Reduzierern und Selektoren abdeckt, anstatt drei Testsätze, die sich auf jeden einzelnen konzentrieren. Dies simuliert genauer, was in Ihrer realen Anwendung passiert, und bietet das beste Preis-Leistungs-Verhältnis.

Wenn ich es weiter aufschlüssele, habe ich festgestellt, dass es nützlich ist, Tests zu schreiben, die sich auf Aktionsersteller konzentrieren, und dann das Ergebnis mit Selektoren zu überprüfen. (Testen Sie Reduzierstücke nicht direkt.) Wichtig ist, dass eine bestimmte Aktion den erwarteten Zustand ergibt. Die Überprüfung dieses Ergebnisses mit Ihren (gemeinsamen) Selektoren ist eine Möglichkeit, alle drei in einem einzigen Durchgang abzudecken.

discussion

Hilfreichster Kommentar

@dtinth @denis-sokolov Da stimme ich dir auch zu. Übrigens, als ich auf das Redux-Saga- Projekt Bezug nahm, habe ich vielleicht nicht deutlich gemacht, dass ich gegen die Idee bin, die ActionCreators wachsen und mit der Zeit immer komplexer werden zu lassen.

Das Redux-saga-Projekt ist auch ein Versuch, das zu tun, was Sie @dtinth beschreiben, aber es gibt einen subtilen Unterschied zu dem, was Sie beide sagen. Es scheint, dass Sie sagen möchten, dass Sie, wenn Sie jedes Rohereignis schreiben, das in das Aktionsprotokoll eingetreten ist, leicht jeden Zustand aus den Reduzierern dieses Aktionsprotokolls berechnen können. Dies ist absolut richtig, und ich bin diesen Weg eine Weile gegangen, bis meine App sehr schwer zu warten war, weil das Aktionsprotokoll nicht explizit wurde und die Reduzierungen im Laufe der Zeit zu komplex wurden.

Vielleicht können Sie sich diesen Punkt der ursprünglichen Diskussion ansehen, die zur Redux-Saga-Diskussion führte: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Anwendungsfall zum Lösen

Stellen Sie sich vor, Sie haben eine Todo-App mit dem offensichtlichen TodoCreated-Ereignis. Dann bitten wir Sie, ein App-Onboarding zu codieren. Sobald der Benutzer ein Todo erstellt hat, sollten wir ihm mit einem Popup gratulieren.

Der "unreine" Weg:

Das scheint @bvaughn zu bevorzugen

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Ich mag diesen Ansatz nicht, weil der Aktionsersteller dadurch stark an das Layout der App-Ansicht gekoppelt ist. Es wird davon ausgegangen, dass der actionCreator die Struktur des UI-Zustandsbaums kennen sollte, um seine Entscheidung zu treffen.

Die Methode "Alles aus Rohereignissen berechnen":

Das scheint @denis-sokolov @dtinth zu bevorzugen:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Ja, Sie können einen Reduzierer erstellen, der weiß, ob die Glückwünsche angezeigt werden sollen. Aber dann haben Sie ein Popup, das angezeigt wird, ohne dass eine Aktion angezeigt wird, die besagt, dass das Popup angezeigt wurde. Nach meiner eigenen Erfahrung (und immer noch mit Legacy-Code) ist es immer besser, es sehr explizit zu machen: NIEMALS das Glückwunsch-Popup anzeigen, wenn keine Aktion DISPLAY_CONGRATULATION ausgelöst wird. Explizit ist viel einfacher zu pflegen als implizit.

Der vereinfachte Saga-Weg.

Die Redux-Saga verwendet Generatoren und sieht vielleicht etwas kompliziert aus, wenn Sie es nicht gewohnt sind, aber im Grunde würden Sie mit einer vereinfachten Implementierung etwa Folgendes schreiben:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

Die Saga ist ein zustandsbehafteter Akteur, der Ereignisse empfängt und Effekte erzeugen kann. Hier ist es als unreiner Reduzierer implementiert, um Ihnen eine Vorstellung davon zu geben, was es ist, aber es ist tatsächlich nicht im Redux-Saga-Projekt enthalten.

Die Regeln etwas verkomplizieren:

Wenn Sie sich um die Anfangsregel kümmern, ist sie nicht über alles sehr explizit.
Wenn Sie sich die obigen Implementierungen ansehen, werden Sie feststellen, dass das Glückwunsch-Popup jedes Mal geöffnet wird, wenn wir während des Onboardings eine Todo erstellen. Höchstwahrscheinlich möchten wir, dass es nur für das erste erstellte Todo geöffnet wird, das während des Onboardings passiert, und nicht für alle. Außerdem möchten wir es dem Benutzer ermöglichen, das Onboarding von Anfang an zu wiederholen.

Können Sie sehen, wie der Code im Laufe der Zeit in allen 3 Implementierungen unordentlich werden würde, wenn das Onboarding immer komplizierter wird?

Der Redux-Saga-Weg

Mit redux-saga und den oben genannten Onboarding-Regeln würdest du so etwas schreiben wie

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Ich denke, es löst diesen Anwendungsfall auf viel einfachere Weise als die oben genannten Lösungen. Wenn ich falsch liege, gib mir bitte deine einfachere Implementierung :)

Sie haben von unreinem Code gesprochen, und in diesem Fall gibt es keine Unreinheit in der Redux-Saga-Implementierung, da die Take/Put-Effekte eigentlich Daten sind. Wenn take() aufgerufen wird, wird es nicht ausgeführt, es gibt einen Deskriptor des auszuführenden Effekts zurück, und irgendwann tritt ein Interpreter ein, sodass Sie keine Nachbildungen benötigen, um die Sagen zu testen. Wenn Sie ein funktionaler Entwickler sind, der Haskell macht, denken Sie an Free / IO-Monaden.


In diesem Fall erlaubt es:

  • Vermeiden Sie es, ActionCreator zu komplizieren und machen Sie es abhängig von getState
  • Machen Sie das Implizite expliziter
  • Vermeiden Sie die Kopplung von transversaler Logik (wie das obige Onboarding) an Ihre Kerngeschäftsdomäne (Erstellung von Aufgaben).

Es kann auch eine Interpretationsebene bereitstellen, die es ermöglicht, Rohereignisse in aussagekräftigere/höhere Ereignisse zu übersetzen (ähnlich wie ELM, indem Ereignisse umhüllt werden, während sie aufsteigen).

Beispiele:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" könnte zu "NEXT_PAGE_LOADED" führen
  • "REQUEST_FAILED" kann zu "USER_DISCONNECTED" führen, wenn der Fehlercode 401 ist.
  • "HASHTAG_FILTER_ADDED" könnte zu "CONTENT_RELOADED" führen

Wenn Sie ein modulares App-Layout mit Enten erreichen möchten, können Sie die Kopplung von Enten vermeiden. Die Saga wird zum Koppelpunkt. Die Enten müssen nur von ihren rohen Ereignissen wissen, und die Saga interpretiert diese rohen Ereignisse. Dies ist weitaus besser, als wenn duck1 Aktionen von duck2 direkt abschickt, weil es das duck1-Projekt einfacher in einem anderen Kontext wiederverwenden lässt. Man könnte jedoch argumentieren, dass der Kopplungspunkt auch in Aktion Schöpfer liegen könnte und dies ist, was die meisten Menschen heute tun.

Alle 106 Kommentare

Neugierig, ob Sie Immutable.js oder andere verwenden. In den wenigen Redux-Dingen, die ich gebaut habe, konnte ich mir nicht vorstellen, Immutable nicht zu verwenden, aber ich habe eine ziemlich tief verschachtelte Struktur, die Immutable zähmt.

Beeindruckend. Was für ein Versehen für mich, das _nicht_ zu erwähnen. Jawohl! Wir verwenden unveränderlich! Wie Sie sagen, ist es schwer vorstellbar, es _nicht_ für etwas Wesentliches zu verwenden.

@bvaughn Ein Bereich, mit dem ich zu kämpfen hatte, ist, die Grenze zwischen Immutable und den Komponenten zu ziehen. Durch die Übergabe unveränderlicher Objekte an die Komponenten können Sie sehr einfach reine Render-Dekoratoren/Mixins verwenden, aber dann erhalten Sie unveränderlichen Code in Ihren Komponenten (was mir nicht gefällt). Bisher habe ich nur nachgegeben und das getan, aber ich vermute, dass Sie Selektoren in den render()-Methoden verwenden, anstatt direkt auf die Methoden von Immutable.js zuzugreifen?

Um ehrlich zu sein ist dies etwas, für das wir noch keine harte Politik definiert haben. Oft verwenden wir Selektoren in unseren "intelligenten" Containern, um native Werte aus unseren unveränderlichen Objekten zu extrahieren, und übergeben die nativen Werte dann als Strings, Booleans usw. an unsere Komponenten. Gelegentlich übergeben wir ein unveränderliches Objekt, aber wenn wir es tun - fast immer übergeben Sie einen Record Typ, damit die Komponente es wie ein natives Objekt (mit Gettern) behandeln kann.

Ich habe mich in die entgegengesetzte Richtung bewegt und Action-Ersteller trivialer gemacht. Aber ich fange gerade erst mit Redux an. Einige Fragen zu Ihrem Ansatz:

1) Wie testest du deine Action Creators? Ich mag es, so viel Logik wie möglich auf reine, synchrone Funktionen zu verschieben, die nicht von externen Diensten abhängig sind, da sie einfacher zu testen sind.
2) Nutzt du die Zeitreise mit heißem Nachladen? Eines der netten Dinge mit den React Redux Devtools ist, dass der Store beim Einrichten des Hot Reloading alle Aktionen für den neuen Reducer erneut ausführt. Wenn ich meine Logik in die Action-Ersteller verlegen würde, würde ich das verlieren.
3) Wenn Ihre Aktionsersteller mehrmals senden, um eine Wirkung zu erzielen, bedeutet dies, dass Ihr Zustand kurzzeitig in einem ungültigen Zustand ist? (Ich denke hier an mehrere synchrone Dispatches, nicht an asynchrone Dispatches zu einem späteren Zeitpunkt)

Verwenden Sie Selektoren überall

Ja, das scheint zu sagen, dass Ihre Reduzierer ein Implementierungsdetail Ihres Zustands sind und dass Sie Ihren Zustand über eine Abfrage-API für Ihre Komponente verfügbar machen.
Wie jede Schnittstelle erlaubt es, den Zustand zu entkoppeln und leicht umzugestalten.

Verwenden Sie ImmutableJS

IMO mit neuer JS-Syntax ist es nicht mehr so ​​nützlich, ImmutableJS zu verwenden, da Sie Listen und Objekte mit normalem JS leicht ändern können. Es sei denn, Sie haben sehr große Listen und Objekte mit vielen Eigenschaften und benötigen aus Leistungsgründen eine strukturelle gemeinsame Nutzung, ImmutableJS ist keine strikte Anforderung.

Mach mehr in AktionCreators

@bvaughn dieses Projekt sollte man sich unbedingt anschauen: https://github.com/yelouafi/redux-saga
Als ich anfing, @yelouafi über Sagas (ursprünglich Backend-Konzept) zu

1) Wie testest du deine Action Creators? Ich mag es, so viel Logik wie möglich auf reine, synchrone Funktionen zu verschieben, die nicht von externen Diensten abhängig sind, da sie einfacher zu testen sind.

Ich habe versucht, dies oben zu beschreiben, aber im Grunde... Ich denke, es ist (für mich bisher) am sinnvollsten, Ihre Action-Schöpfer mit einem "Enten"-ähnlichen Ansatz zu testen. Beginnen Sie einen Test, indem Sie das Ergebnis eines Aktionserstellers verteilen und dann den Zustand mit Selektoren überprüfen. Auf diese Weise können Sie mit einem einzigen Test den Action-Creator, seine Reduzierer und alle zugehörigen Selektoren abdecken.

2) Nutzt du die Zeitreise mit heißem Nachladen? Eines der netten Dinge mit den React Redux Devtools ist, dass der Store beim Einrichten des Hot Reloading alle Aktionen für den neuen Reducer erneut ausführt. Wenn ich meine Logik in die Action-Ersteller verlegen würde, würde ich das verlieren.

Nein, wir verwenden keine Zeitreisen. Aber warum sollte Ihre Geschäftslogik in einem Action-Creator hier einen Einfluss haben? Das einzige, was den Status Ihrer Anwendung aktualisiert, sind Ihre Reduzierstücke. Das erneute Ausführen der erstellten Aktionen würde in beiden Fällen das gleiche Ergebnis erzielen.

3) Wenn Ihre Aktionsersteller mehrmals senden, um eine Wirkung zu erzielen, bedeutet dies, dass Ihr Zustand kurzzeitig in einem ungültigen Zustand ist? (Ich denke hier an mehrere synchrone Dispatches, nicht an asynchrone Dispatches zu einem späteren Zeitpunkt)

Ein vorübergehender ungültiger Zustand ist etwas, das Sie in einigen Fällen nicht wirklich vermeiden können. Solange es schließlich Konsistenz gibt, ist es normalerweise kein Problem. Und auch hier könnte Ihr Status vorübergehend ungültig sein, unabhängig davon, ob Ihre Geschäftslogik in den Aktionserstellern oder Reduzierern enthalten ist. Es hat mehr mit Nebenwirkungen und den Besonderheiten Ihres Shops zu tun.

IMO mit neuer JS-Syntax ist es nicht mehr so ​​nützlich, ImmutableJS zu verwenden, da Sie Listen und Objekte mit normalem JS leicht ändern können. Es sei denn, Sie haben sehr große Listen und Objekte mit vielen Eigenschaften und benötigen aus Leistungsgründen eine strukturelle gemeinsame Nutzung, ImmutableJS ist keine strikte Anforderung.

Die Hauptgründe für die Verwendung von Immutable (in meinen Augen) sind nicht die Leistung oder der syntaktische Zucker für Updates. Der Hauptgrund ist, dass es Sie (oder jemand anderes) daran hindert, _aus Versehen_ den eingehenden Zustand innerhalb eines Reducers zu verändern. Das ist ein No-Go und ist mit einfachen JS-Objekten leider einfach zu bewerkstelligen.

@bvaughn dieses Projekt sollte man sich unbedingt anschauen: https://github.com/yelouafi/redux-saga
Als ich anfing, @yelouafi über Sagas (ursprünglich Backend-Konzept) zu

Ich habe dieses Projekt tatsächlich schon einmal ausgecheckt :) Obwohl ich es noch nicht verwendet habe. Es sieht ordentlich aus.

Ich habe versucht, dies oben zu beschreiben, aber im Grunde... Ich denke, es ist (für mich bisher) am sinnvollsten, Ihre Action-Schöpfer mit einem "Enten"-ähnlichen Ansatz zu testen. Beginnen Sie einen Test, indem Sie das Ergebnis eines Aktionserstellers verteilen und dann den Zustand mit Selektoren überprüfen. Auf diese Weise können Sie mit einem einzigen Test den Action-Creator, seine Reduzierer und alle zugehörigen Selektoren abdecken.

Entschuldigung, ich habe das Teil. Worüber ich mich gewundert habe, ist der Teil von Tests, der mit der Asynchronität interagiert. Ich könnte einen Test so schreiben:

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

Aber wie sieht dein Test aus? Etwas wie das?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

Nein, wir verwenden keine Zeitreisen. Aber warum sollte Ihre Geschäftslogik in einem Action-Creator hier einen Einfluss haben? Das einzige, was den Status Ihrer Anwendung aktualisiert, sind Ihre Reduzierstücke. Das erneute Ausführen der erstellten Aktionen würde in beiden Fällen das gleiche Ergebnis erzielen.

Was ist, wenn der Code geändert wird? Wenn ich den Reduzierer ändere, werden die gleichen Aktionen wiederholt, jedoch mit dem neuen Reduzierer. Wenn ich hingegen den Aktionsersteller ändere, werden die neuen Versionen nicht wiedergegeben. Betrachten Sie also zwei Szenarien:

Mit Reduzierstück:

1) Ich versuche eine Aktion in meiner App.
2) Es gibt einen Fehler in meinem Reduzierer, der zu einem falschen Zustand führt.
3) Ich behebe den Fehler im Reducer und speichere
4) Zeitreisen lädt den neuen Reduzierer und versetzt mich in den Zustand, in dem ich hätte sein sollen.

Wobei mit einem Action Creator

1) Ich versuche eine Aktion in meiner App.
2) Es gibt einen Fehler im Aktionsersteller, der dazu führt, dass die falsche Aktion erstellt wird
3) Ich behebe den Fehler im Action Creator und speichere
4) Ich befinde mich immer noch im falschen Zustand, was von mir erfordert, dass ich die Aktion zumindest noch einmal versuche und möglicherweise aktualisieren muss, wenn sie mich in einen völlig defekten Zustand versetzt.

Ein vorübergehender ungültiger Zustand ist etwas, das Sie in einigen Fällen nicht wirklich vermeiden können. Solange es schließlich Konsistenz gibt, ist es normalerweise kein Problem. Und auch hier könnte Ihr Status vorübergehend ungültig sein, unabhängig davon, ob Ihre Geschäftslogik in den Aktionserstellern oder Reduzierern enthalten ist. Es hat mehr mit Nebenwirkungen und den Besonderheiten Ihres Shops zu tun.

Ich denke, meine Denkweise über Redux besteht darin, dass der Laden immer in einem gültigen Zustand ist. Der Reduzierer nimmt immer einen gültigen Zustand an und erzeugt einen gültigen Zustand. Welche Fälle, glauben Sie, zwingen einen dazu, einige inkonsistente Zustände zuzulassen?

Entschuldigung für die Unterbrechung, aber was meinst du mit ungültigen und gültigen Zuständen?
Hier? Daten werden geladen oder Aktion ausgeführt, aber noch nicht abgeschlossen aussehen
wie ein gültiger vorübergehender Zustand für mich.

Was meinst du mit transientem Zustand in Redux @bvaughn und @sompylasar ? Entweder ist der Dispatch beendet, oder er wirft. Wenn es geworfen wird, ändert sich der Zustand nicht.

Sofern Ihr Reduzierer keine Codeprobleme hat, verfügt Redux nur über Zustände, die mit der Reduziererlogik übereinstimmen. Irgendwie werden alle gesendeten Aktionen in einer Transaktion behandelt: Entweder wird der gesamte Baum aktualisiert oder der Zustand ändert sich überhaupt nicht.

Wenn der gesamte Baum aktualisiert wird, aber nicht auf angemessene Weise (wie ein Zustand, den React nicht rendern kann), haben Sie Ihre Arbeit einfach nicht richtig gemacht :)

In Redux ist der aktuelle Zustand zu berücksichtigen, dass ein einzelner Dispatch eine Transaktionsgrenze ist.

Ich verstehe jedoch das Anliegen von @winstonewert, dass anscheinend 2 Aktionen synchron in einer gleichen Transaktion versenden wollen. Denn manchmal lösen actionCreators mehrere Aktionen aus und erwarten, dass alle Aktionen korrekt ausgeführt werden. Wenn 2 Aktionen ausgeführt werden und dann die zweite fehlschlägt, wird nur die erste angewendet, was zu einem Zustand führt, den wir als "inkonsistent" bezeichnen könnten. Vielleicht möchte @winstonewert, dass, wenn der Versand der 2. Aktion fehlschlägt, wir die 2 Aktionen zurücksetzen.

@winstonewert Ich habe so etwas in unserem internen Framework hier implementiert und es funktioniert bis jetzt https://github.com/stample/atom-react/blob/master/src/atom/atom.js
Ich wollte auch Rendering-Fehler behandeln: Wenn ein Zustand nicht erfolgreich gerendert werden kann, wollte ich, dass mein Zustand zurückgesetzt wird, um eine Blockierung der Benutzeroberfläche zu vermeiden. Leider macht React bis zum nächsten Release einen sehr schlechten Job, wenn die Rendermethoden Fehler ausgeben, daher war es nicht so nützlich, könnte es aber in Zukunft sein.

Ich bin mir ziemlich sicher, dass wir einem Geschäft erlauben können, mehrere Sync-Dispatches in einer Transaktion mit einer Middleware zu akzeptieren.

Ich bin mir jedoch nicht sicher, ob es möglich wäre, den Status im Falle eines Renderfehlers zurückzusetzen, da der Redux-Speicher im Allgemeinen bereits "commitiert" hat, wenn wir versuchen, seinen Status zu rendern. In meinem Framework gibt es einen "beforeTransactionCommit" -Hook, den ich verwende, um das Rendern auszulösen und bei jedem Renderfehler schließlich ein Rollback durchzuführen.

@gaearon Ich frage mich, ob Sie planen, diese Art von Funktionen zu unterstützen und ob dies mit der aktuellen API möglich wäre.

Es scheint mir, dass Redux-Batched-Subscribe keine echten Transaktionen erlaubt, sondern nur die Anzahl der Renderings reduziert. Was ich sehe ist, dass der Store nach jedem Versand "Commit" macht, auch wenn der Abo-Listener nur einmal am Ende gefeuert wird

Warum brauchen wir eine vollständige Transaktionsunterstützung? Ich glaube, ich verstehe den Anwendungsfall nicht.

@gaearon Ich bin mir noch nicht ganz sicher, würde mich aber freuen, mehr über den Anwendungsfall von

Die Idee ist, dass Sie dispatch([a1,a2]) ausführen können und wenn a2 fehlschlägt, dann wird ein Rollback in den Zustand vor dem Versand von a1 durchgeführt.

In der Vergangenheit habe ich oft mehrere Aktionen synchron gesendet (z. B. auf einem einzelnen onClick-Listener oder in einem actionCreator) und Transaktionen hauptsächlich implementiert, um render nur am Ende aller gesendeten Aktionen aufzurufen, aber das war so anders gelöst durch das redux-batched-subscribe-Projekt.

In meinen Anwendungsfällen dienten die Aktionen, die ich für eine Transaktion ausgelöst habe, hauptsächlich dazu, unnötige Renderings zu vermeiden, aber die Aktionen machten unabhängig voneinander Sinn. aber vielleicht nicht die geplante...). Ich weiß nicht wirklich, ob jemand einen Anwendungsfall finden kann, bei dem ein vollständiges Rollback nützlich wäre

Wenn das Rendern jedoch fehlschlägt, ist es nicht sinnvoll, ein Rollback auf den letzten Zustand durchzuführen, für den das Rendern nicht fehlschlägt, anstatt zu versuchen, einen nicht Rendering-Zustand zu erreichen?

Würde ein einfacher Reducer Enhancer funktionieren? z.B

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

Damit können Sie ein Array an den Store senden. Der Enhancer entpackt einzelne Aktionen und führt sie jedem Reducer zu.

Ohh @gaearon das wusste ich nicht. Mir ist nicht aufgefallen, dass es 2 verschiedene Projekte gibt, die versuchen, einen ziemlich ähnlichen Anwendungsfall auf unterschiedliche Weise zu lösen:

Beide ermöglichen es, unnötige Renderings zu vermeiden, aber die erste würde alle Batch-Aktionen zurücksetzen, während die zweite nur die fehlgeschlagene Aktion nicht anwendet.

@gaearon Autsch, mein angeschaut habe. :gespült:


Action Creators stehen für unreinen Code

Ich habe wahrscheinlich nicht so viel praktische Erfahrung mit Redux wie die meisten Leute, aber auf den ersten Blick muss ich der Aussage „Mehr bei Action-Erstellern und weniger bei Reduzierern“ widersprechen Gesellschaft.

In Hacker Way: Rethinking Web App Development bei Facebook, wo das Flux-Muster eingeführt wird, ist das eigentliche Problem, das zur Erfindung von Flux führt, zwingender Code.

In diesem Fall ist ein Action Creator, der I/O durchführt, dieser zwingende Code.

Wir verwenden Redux bei der Arbeit nicht, aber bei mir haben wir früher feinkörnige Aktionen (die natürlich alle für sich alleine Sinn machen) und sie in einem Stapel ausgelöst. Wenn Sie beispielsweise auf eine Nachricht klicken, werden drei Aktionen ausgelöst: OPEN_MESSAGE_VIEW , FETCH_MESSAGE , MARK_NOTIFICATION_AS_READ .

Dann stellt sich heraus, dass diese „Low-Level“-Aktionen nicht mehr als ein „Befehl“ oder „Setter“ oder „Nachricht“ sind, um einen Wert in einem Geschäft zu setzen. Wir könnten genauso gut zurückgehen und MVC verwenden und mit einfacherem Code enden, wenn wir so weitermachen.

In gewisser Weise repräsentieren Action Creators unreinen Code, während Reducer (und Selectors) reinen Code repräsentieren . Haskell-Leute haben herausgefunden, dass es besser ist, weniger unreinen Code und mehr reinen Code zu verwenden .

In meinem Nebenprojekt (mit Redux) verwende ich beispielsweise die Spracherkennungs-API von Webkit. Es gibt onresult Ereignis aus, während Sie sprechen. Es gibt zwei Möglichkeiten – wo werden diese Ereignisse verarbeitet?

  • Lassen Sie den Aktionsersteller das Ereignis zuerst verarbeiten und dann an den Store senden.
  • Senden Sie einfach das Ereignisobjekt in den Store.

Ich ging mit Nummer zwei: Senden Sie einfach das Rohereignisobjekt in den Speicher.

Es scheint jedoch, dass Redux-Entwicklungstools es nicht mögen, wenn nicht-einfache Objekte in den Speicher gesendet werden, daher habe ich im Aktionsersteller eine kleine Logik hinzugefügt, um diese Ereignisobjekte in einfache Objekte umzuwandeln. (Der Code im Action Creator ist so trivial, dass er nicht schief gehen kann.)

Dann kann der Reducer diese sehr primitiven Ereignisse kombinieren und das Transkript des Gesprochenen aufbauen. Da diese Logik in reinem Code lebt, kann ich sie sehr einfach live optimieren (indem ich den Reduzierer im laufenden Betrieb neu lade).

Ich möchte @dtinth unterstützen. Aktionen sollten Ereignisse darstellen, die aus der realen Welt passiert sind, und nicht, wie wir auf diese Ereignisse reagieren möchten. Siehe insbesondere CQRS: Wir möchten möglichst viele Details zu realen Ereignissen protokollieren, und wahrscheinlich werden die Reduzierer in Zukunft verbessert und alte Ereignisse mit neuer Logik verarbeitet.

@dtinth @denis-sokolov Da stimme ich dir auch zu. Übrigens, als ich auf das Redux-Saga- Projekt Bezug nahm, habe ich vielleicht nicht deutlich gemacht, dass ich gegen die Idee bin, die ActionCreators wachsen und mit der Zeit immer komplexer werden zu lassen.

Das Redux-saga-Projekt ist auch ein Versuch, das zu tun, was Sie @dtinth beschreiben, aber es gibt einen subtilen Unterschied zu dem, was Sie beide sagen. Es scheint, dass Sie sagen möchten, dass Sie, wenn Sie jedes Rohereignis schreiben, das in das Aktionsprotokoll eingetreten ist, leicht jeden Zustand aus den Reduzierern dieses Aktionsprotokolls berechnen können. Dies ist absolut richtig, und ich bin diesen Weg eine Weile gegangen, bis meine App sehr schwer zu warten war, weil das Aktionsprotokoll nicht explizit wurde und die Reduzierungen im Laufe der Zeit zu komplex wurden.

Vielleicht können Sie sich diesen Punkt der ursprünglichen Diskussion ansehen, die zur Redux-Saga-Diskussion führte: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Anwendungsfall zum Lösen

Stellen Sie sich vor, Sie haben eine Todo-App mit dem offensichtlichen TodoCreated-Ereignis. Dann bitten wir Sie, ein App-Onboarding zu codieren. Sobald der Benutzer ein Todo erstellt hat, sollten wir ihm mit einem Popup gratulieren.

Der "unreine" Weg:

Das scheint @bvaughn zu bevorzugen

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Ich mag diesen Ansatz nicht, weil der Aktionsersteller dadurch stark an das Layout der App-Ansicht gekoppelt ist. Es wird davon ausgegangen, dass der actionCreator die Struktur des UI-Zustandsbaums kennen sollte, um seine Entscheidung zu treffen.

Die Methode "Alles aus Rohereignissen berechnen":

Das scheint @denis-sokolov @dtinth zu bevorzugen:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Ja, Sie können einen Reduzierer erstellen, der weiß, ob die Glückwünsche angezeigt werden sollen. Aber dann haben Sie ein Popup, das angezeigt wird, ohne dass eine Aktion angezeigt wird, die besagt, dass das Popup angezeigt wurde. Nach meiner eigenen Erfahrung (und immer noch mit Legacy-Code) ist es immer besser, es sehr explizit zu machen: NIEMALS das Glückwunsch-Popup anzeigen, wenn keine Aktion DISPLAY_CONGRATULATION ausgelöst wird. Explizit ist viel einfacher zu pflegen als implizit.

Der vereinfachte Saga-Weg.

Die Redux-Saga verwendet Generatoren und sieht vielleicht etwas kompliziert aus, wenn Sie es nicht gewohnt sind, aber im Grunde würden Sie mit einer vereinfachten Implementierung etwa Folgendes schreiben:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

Die Saga ist ein zustandsbehafteter Akteur, der Ereignisse empfängt und Effekte erzeugen kann. Hier ist es als unreiner Reduzierer implementiert, um Ihnen eine Vorstellung davon zu geben, was es ist, aber es ist tatsächlich nicht im Redux-Saga-Projekt enthalten.

Die Regeln etwas verkomplizieren:

Wenn Sie sich um die Anfangsregel kümmern, ist sie nicht über alles sehr explizit.
Wenn Sie sich die obigen Implementierungen ansehen, werden Sie feststellen, dass das Glückwunsch-Popup jedes Mal geöffnet wird, wenn wir während des Onboardings eine Todo erstellen. Höchstwahrscheinlich möchten wir, dass es nur für das erste erstellte Todo geöffnet wird, das während des Onboardings passiert, und nicht für alle. Außerdem möchten wir es dem Benutzer ermöglichen, das Onboarding von Anfang an zu wiederholen.

Können Sie sehen, wie der Code im Laufe der Zeit in allen 3 Implementierungen unordentlich werden würde, wenn das Onboarding immer komplizierter wird?

Der Redux-Saga-Weg

Mit redux-saga und den oben genannten Onboarding-Regeln würdest du so etwas schreiben wie

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Ich denke, es löst diesen Anwendungsfall auf viel einfachere Weise als die oben genannten Lösungen. Wenn ich falsch liege, gib mir bitte deine einfachere Implementierung :)

Sie haben von unreinem Code gesprochen, und in diesem Fall gibt es keine Unreinheit in der Redux-Saga-Implementierung, da die Take/Put-Effekte eigentlich Daten sind. Wenn take() aufgerufen wird, wird es nicht ausgeführt, es gibt einen Deskriptor des auszuführenden Effekts zurück, und irgendwann tritt ein Interpreter ein, sodass Sie keine Nachbildungen benötigen, um die Sagen zu testen. Wenn Sie ein funktionaler Entwickler sind, der Haskell macht, denken Sie an Free / IO-Monaden.


In diesem Fall erlaubt es:

  • Vermeiden Sie es, ActionCreator zu komplizieren und machen Sie es abhängig von getState
  • Machen Sie das Implizite expliziter
  • Vermeiden Sie die Kopplung von transversaler Logik (wie das obige Onboarding) an Ihre Kerngeschäftsdomäne (Erstellung von Aufgaben).

Es kann auch eine Interpretationsebene bereitstellen, die es ermöglicht, Rohereignisse in aussagekräftigere/höhere Ereignisse zu übersetzen (ähnlich wie ELM, indem Ereignisse umhüllt werden, während sie aufsteigen).

Beispiele:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" könnte zu "NEXT_PAGE_LOADED" führen
  • "REQUEST_FAILED" kann zu "USER_DISCONNECTED" führen, wenn der Fehlercode 401 ist.
  • "HASHTAG_FILTER_ADDED" könnte zu "CONTENT_RELOADED" führen

Wenn Sie ein modulares App-Layout mit Enten erreichen möchten, können Sie die Kopplung von Enten vermeiden. Die Saga wird zum Koppelpunkt. Die Enten müssen nur von ihren rohen Ereignissen wissen, und die Saga interpretiert diese rohen Ereignisse. Dies ist weitaus besser, als wenn duck1 Aktionen von duck2 direkt abschickt, weil es das duck1-Projekt einfacher in einem anderen Kontext wiederverwenden lässt. Man könnte jedoch argumentieren, dass der Kopplungspunkt auch in Aktion Schöpfer liegen könnte und dies ist, was die meisten Menschen heute tun.

@slorber Dies ist ein hervorragendes Beispiel! Vielen Dank, dass Sie sich die Zeit genommen haben, die Vor- und Nachteile der einzelnen Ansätze klar zu erläutern. (Ich denke sogar, das sollte in die Dokumentation gehen.)

Früher habe ich eine ähnliche Idee untersucht (die ich "Arbeiterkomponenten" nannte). Im Grunde ist es eine React-Komponente, die nichts rendert ( render: () => null ), sondern auf Ereignisse (zB aus Geschäften) lauscht und andere Nebenwirkungen auslöst. Diese Worker-Komponente wird dann in die Anwendungsstammkomponente eingefügt. Nur eine weitere verrückte Art, mit komplexen Nebenwirkungen umzugehen. :stuck_out_tongue:

Viele Diskussionen hier, während ich schlief.

@winstonewert , Sie

@dtinth , es tut mir leid, aber ich


@winstonewert sagte: "Ich denke, meine Denkweise über Redux besteht darin, dass der Laden immer in einem gültigen Zustand ist."
@slorber fragte: "Was meinst du mit transientem Zustand in Redux @bvaughn und @sompylasar ? Auch wenn der Dispatch beendet oder ausgelöst wird. Wenn er wirft, ändert sich der Zustand nicht."

Ich bin mir ziemlich sicher, dass wir über verschiedene Dinge nachdenken. Als ich "vorübergehender ungültiger Zustand" sagte, bezog ich mich auf einen Anwendungsfall wie den folgenden. Zum Beispiel haben ich und ein Kollege kürzlich redux-search veröffentlicht . Diese Such-Middleware lauscht auf Änderungen an Sammlungen von durchsuchbaren Dingen und indiziert sie dann für die Suche (neu). Wenn der Benutzer Filtertext angibt, gibt redux-search die Liste der Ressourcen-UIDs zurück, die dem Text des Benutzers entsprechen. Betrachten Sie also Folgendes:

Stellen Sie sich vor, Ihr Anwendungsspeicher enthält einige durchsuchbare Objekte: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . Der Benutzer hat den Filtertext "e" eingegeben und so enthält die Suchmiddleware ein Array von IDs 1 und 3. Stellen Sie sich nun vor, dass Benutzer 1 (Alex) gelöscht wird - entweder als Reaktion auf eine lokale Benutzeraktion oder eine Aktualisierung von Remote-Daten, die enthält diesen Benutzerdatensatz nicht mehr. An dem Punkt, an dem Ihr Reducer die Benutzersammlung aktualisiert, ist Ihr Shop vorübergehend ungültig, da redux-search auf eine ID verweist, die in der Sammlung nicht mehr vorhanden ist. Sobald die Middleware erneut ausgeführt wurde, korrigiert sie den ungültigen Status. So etwas kann immer passieren, wenn ein Knoten Ihres Baums mit einem anderen Knoten verknüpft ist.


@slorber sagte: "Ich mag diesen Ansatz nicht, weil er den Aktionsersteller stark mit dem Layout der App-Ansicht verknüpft. Es wird davon ausgegangen, dass der Aktionsersteller die Struktur des UI-Zustandsbaums kennen sollte, um seine Entscheidung zu treffen."

Ich verstehe nicht, was Sie mit dem Ansatz meinen, den Action-Creator "an das Layout der App-Ansicht" zu koppeln. Der Zustandsbaum _antrieb_ (oder informiert) die Benutzeroberfläche. Das ist einer der Hauptzwecke von Flux. Und Ihre Aktionsersteller und -reduzierer sind per Definition mit diesem Zustand verbunden (aber nicht mit der Benutzeroberfläche).

Für das, was es wert ist, ist der Beispielcode, den Sie als etwas, das ich bevorzuge, geschrieben haben, nicht das, was ich im Sinn hatte. Vielleicht habe ich mich schlecht erklärt. Ich denke, eine Schwierigkeit bei der Diskussion über so etwas besteht darin, dass es sich normalerweise nicht in einfachen oder allgemeinen Beispielen manifestiert. (Zum Beispiel ist die TODO MVC-App standardmäßig nicht komplex genug für solche nuancierten Diskussionen.)

Zur Verdeutlichung im letzten Punkt

Übrigens @slorber hier ist ein Beispiel für das, was ich im Sinn hatte. Es ist ein bisschen konstruiert.

Nehmen wir an, Ihr Bundesstaat hat viele Knoten. Einer dieser Knoten speichert gemeinsam genutzte Ressourcen. (Mit "gemeinsam" meine ich Ressourcen, die lokal zwischengespeichert wurden und auf die von mehreren Seiten innerhalb Ihrer Anwendung zugegriffen wird.) Diese gemeinsam genutzten Ressourcen haben ihre eigenen Aktionsersteller und -reduzierer ("Enten"). Ein anderer Knoten speichert Informationen für eine bestimmte Anwendungsseite. Ihre Seite hat auch eine eigene Ente.

Nehmen wir an, Ihre Seite muss das neueste und beste Ding laden und einem Benutzer dann erlauben, es zu bearbeiten. Hier ist ein Beispiel für einen Action-Creator-Ansatz, den ich für eine solche Situation verwenden könnte:

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

Vielleicht möchte @winstonewert, dass, wenn der Versand der 2. Aktion fehlschlägt, wir die 2 Aktionen zurücksetzen.

Nein. Ich würde keinen Aktionsersteller schreiben, der zwei Aktionen auslöst. Ich würde eine einzelne Aktion definieren, die zwei Dinge bewirkt. Das OP scheint Aktionsersteller zu bevorzugen, die kleinere Aktionen ausführen, was die vorübergehenden ungültigen Zustände ermöglicht, die ich nicht mag.

An dem Punkt, an dem Ihr Reducer die Benutzersammlung aktualisiert, ist Ihr Shop vorübergehend ungültig, da redux-search auf eine ID verweist, die in der Sammlung nicht mehr vorhanden ist. Sobald die Middleware erneut ausgeführt wurde, korrigiert sie den ungültigen Status. So etwas kann immer passieren, wenn ein Knoten Ihres Baums mit einem anderen Knoten verknüpft ist.

Das ist tatsächlich der Fall, der mich stört. Meiner Meinung nach wäre der Index idealerweise etwas, das vollständig vom Reduzierer oder einem Selektor behandelt wird. Zusätzliche Aktionen ausführen zu müssen, um die Suche auf dem neuesten Stand zu halten, scheint eine weniger reine Verwendung von Redux zu sein.

Das OP scheint Aktionsersteller zu bevorzugen, die kleinere Aktionen ausführen, was die vorübergehenden ungültigen Zustände ermöglicht, die ich nicht mag.

Nicht genau. Ich würde einzelne Aktionen bevorzugen, wenn es um den Knoten Ihres Aktionserstellers des Zustandsbaums geht. Wenn sich jedoch eine einzelne konzeptionelle Benutzeraktion auf mehrere Knoten des Zustandsbaums auswirkt, müssen Sie mehrere Aktionen ausführen. Sie können jede Aktion separat aufrufen (was meiner Meinung nach _schlecht_ ist) oder Sie könnten einen einzelnen Aktionsersteller die Aktionen ausführen lassen (der Redux-Thunk-Weg, der meiner Meinung nach _besser_ ist, weil er diese Informationen vor Ihrer Ansichtsebene verbirgt).

Das ist tatsächlich der Fall, der mich stört. Meiner Meinung nach wäre der Index idealerweise etwas, das vollständig vom Reduzierer oder einem Selektor behandelt wird. Zusätzliche Aktionen ausführen zu müssen, um die Suche auf dem neuesten Stand zu halten, scheint eine weniger reine Verwendung von Redux zu sein.

Sie senden keine zusätzlichen Aktionen. Die Suche ist eine Middleware. Es ist automatisch. Es gibt jedoch einen vorübergehenden Zustand, wenn die beiden Knoten Ihres Baums nicht übereinstimmen.

@bvaughn Oh, tut mir leid, dass ich so ein Purist bin!

Nun, unreiner Code hat mit dem Abrufen von Daten und anderen Nebeneffekten/IO zu tun, während reiner Code keine Nebeneffekte auslösen kann. In dieser Tabelle finden Sie einen Vergleich zwischen reinem und unreinem Code.

Die Best Practices von Flux besagen, dass eine Aktion „die Aktion eines Benutzers beschreiben sollte, keine Setter sind“. Flux-Dokumente deuteten auch weiter an, woher diese Aktionen kommen sollen:

Wenn neue Daten in das System gelangen, sei es durch eine Person, die mit der Anwendung interagiert, oder durch einen Web-API-Aufruf , werden diese Daten in eine Aktion

Grundsätzlich handelt es sich bei Handlungen um Fakten/Daten, die beschreiben, „was passiert ist“, nicht was passieren sollte. Geschäfte können auf diese Aktionen nur synchron, vorhersehbar und ohne sonstige Nebeneffekte reagieren. Alle anderen Nebenwirkungen sollten in Action Creators (oder Sagas :wink:) behandelt werden.

Beispiel

Ich sage nicht, dass dies der beste Weg ist oder besser als jeder andere Weg oder sogar ein guter Weg. Aber das ist das, was ich derzeit als Best Practice betrachte.

Angenommen, der Benutzer möchte die Anzeigetafel anzeigen, die eine Verbindung zu einem Remote-Server erfordert. Folgendes sollte passieren:

  • Der Benutzer klickt auf die Schaltfläche Scoreboard anzeigen.
  • Die Scoreboard-Ansicht wird mit einer Ladeanzeige angezeigt.
  • Es wird eine Anforderung an den Server gesendet, die Anzeigetafel abzurufen.
  • Warten Sie auf die Antwort.
  • Wenn es erfolgreich ist, zeigen Sie die Anzeigetafel an.
  • Wenn dies fehlschlägt, wird die Anzeigetafel geschlossen und ein Meldungsfeld mit einer Fehlermeldung angezeigt. Der Benutzer kann es schließen.
  • Der Benutzer kann die Anzeigetafel schließen.

Angenommen, Aktionen können den Store nur als Ergebnis einer Aktion des Benutzers oder einer Serverantwort erreichen, können wir 5 Aktionen erstellen.

  • SCOREBOARD_VIEW (als Ergebnis des Benutzers, der auf die Schaltfläche Scoreboard anzeigen klickt)
  • SCOREBOARD_FETCH_SUCCESS (als Ergebnis einer erfolgreichen Antwort vom Server)
  • SCOREBOARD_FETCH_FAILURE (infolge einer Fehlerantwort des Servers)
  • SCOREBOARD_CLOSE (als Ergebnis des Benutzers, der auf die Schließen-Schaltfläche klickt)
  • MESSAGE_BOX_CLOSE (als Ergebnis des Benutzers, der auf die Schließen-Schaltfläche im Meldungsfeld klickt)

Diese 5 Aktionen reichen aus, um alle oben genannten Anforderungen zu erfüllen. Sie sehen, dass die ersten 4 Aktionen nichts mit einer "Ente" zu tun haben. Jede Aktion beschreibt nur, was in der Außenwelt passiert ist (Benutzer möchte dies tun, Server sagte das) und kann von jedem Reduzierer konsumiert werden. Wir haben auch keine MESSAGE_BOX_OPEN Aktion, denn das ist nicht „was passiert“ (obwohl es so sein sollte).

Die einzige Möglichkeit, den Zustandsbaum zu ändern, besteht darin, eine Aktion auszugeben, ein Objekt, das beschreibt, README von Redux

Sie sind mit diesen Action-Erstellern zusammengeklebt:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

Dann kann jeder Teil des Speichers (der von Reduzierern gesteuert wird) auf diese Aktionen reagieren:

| Teil von Store/Reduzierer | Verhalten |
| --- | --- |
| AnzeigetafelAnsicht | Aktualisieren Sie die Sichtbarkeit auf true für SCOREBOARD_VIEW , false für SCOREBOARD_CLOSE und SCOREBOARD_FETCH_FAILURE |
| AnzeigetafelLoadingIndikator | Update - Sichtbarkeit auf true SCOREBOARD_VIEW , falsch auf SCOREBOARD_FETCH_* |
| AnzeigetafelDaten | Daten im Store auf SCOREBOARD_FETCH_SUCCESS aktualisieren |
| MessageBox | Aktualisieren Sie die Sichtbarkeit auf "true" und speichern Sie die Nachricht auf SCOREBOARD_FETCH_FAILURE , und aktualisieren Sie die Sichtbarkeit auf "false" auf MESSAGE_BOX_CLOSE |

Wie Sie sehen, kann sich eine einzelne Aktion auf viele Bereiche des Shops auswirken. Stores erhalten nur eine allgemeine Beschreibung einer Aktion (was ist passiert?) und nicht ein Befehl (was ist zu tun?). Als Ergebnis:

  1. Es ist einfacher, Fehler zu lokalisieren.

Nichts kann den Status des Meldungsfelds beeinflussen. Niemand kann es aus irgendeinem Grund öffnen. Es reagiert nur auf das, was es abonniert hat (Benutzeraktionen und Serverantworten).

Wenn der Server beispielsweise die Anzeigetafel nicht abrufen kann und kein Meldungsfeld angezeigt wurde, müssen Sie nicht herausfinden, warum die Aktion SHOW_MESSAGE_BOX nicht ausgelöst wird. Es wird offensichtlich, dass das Meldungsfeld die Aktion SCOREBOARD_FETCH_FAILURE richtig verarbeitet hat.

Ein Fix ist trivial und kann heiß nachgeladen und zeitversetzt werden.

  1. Action Creator und Reducer können separat getestet werden.

Sie können testen, ob Aktionsmacher das Geschehen in der Außenwelt richtig beschrieben haben, ohne Rücksicht darauf, wie Geschäfte darauf reagieren.

Ebenso lässt sich bei Reduzierern einfach testen, ob sie richtig auf die Einwirkungen der Außenwelt reagieren.

(Ein Integrationstest wäre trotzdem sehr sinnvoll.)

Kein Problem. :) Ich freue mich über die weitere Klarstellung. Es klingt tatsächlich so, als wären wir uns hier einig. Wenn Sie sich Ihren Beispiel-Aktionsersteller viewScoreboard , sieht er meinem Beispiel-Aktionsersteller fetchAndProcessThing sehr ähnlich, direkt darüber.

Action Creator und Reducer können separat getestet werden.

Obwohl ich dem zustimme, denke ich, dass es oft pragmatischer ist, sie gemeinsam zu testen. Es ist wahrscheinlich, dass entweder Ihre Aktion _oder_ oder Ihr Reduzierer (vielleicht beides) sehr einfach ist und daher der Return-on-Effort-Wert des isolierten Testens des Einfachen ziemlich niedrig ist. Aus diesem Grund habe ich vorgeschlagen, den Action-Creator, den Reducer und die zugehörigen Selektoren zusammen zu testen (als "Ente").

Wenn sich jedoch eine einzelne konzeptionelle Benutzeraktion auf mehrere Knoten des Zustandsbaums auswirkt, müssen Sie mehrere Aktionen ausführen.

Genau hier unterscheidet sich meiner Meinung nach das, was Sie tun, von dem, was als Best Practices für Redux angesehen wird. Ich denke, der Standardweg besteht darin, eine Aktion zu haben, auf die mehrere Knoten des Zustandsbaums reagieren.

Ah, interessante Beobachtung @winstonewert. Wir sind einem Muster gefolgt, bei dem für jedes "Enten"-Bundle eindeutige Typkonstanten verwendet werden, und somit nur eine Reduzierer-Reaktion auf Aktionen, die von seinen Geschwister-Aktionserstellern ausgelöst werden. Ich bin mir ehrlich gesagt nicht sicher, wie ich anfangs über willkürliche Reduzierer denke, die auf eine Aktion reagieren. Es fühlt sich an wie eine schlechte Kapselung.

Wir sind einem Muster gefolgt, bei dem für jedes "Enten"-Bündel eindeutige Typkonstanten verwendet werden

Beachten Sie, dass wir es nirgendwo in den Dokumenten befürworten ;-) Ich sage nicht, dass es schlecht ist, aber es gibt den Leuten gewisse manchmal falsche Vorstellungen über Redux.

Daher reagiert ein Reducer nur auf Aktionen, die von seinen Geschwister-Aktionserstellern gesendet werden

In Redux gibt es keine Reduzierer/Action-Ersteller-Paarung. Das ist eine reine Ducks-Sache. Manche mögen es, aber es verschleiert die grundlegenden Stärken des Redux/Flux-Modells: Zustandsmutationen sind voneinander und vom Code, der sie verursacht, entkoppelt.

Ich bin mir ehrlich gesagt nicht sicher, wie ich anfangs über willkürliche Reduzierer denke, die auf eine Aktion reagieren. Es fühlt sich an wie eine schlechte Kapselung.

Je nachdem, was Sie als Kapselungsgrenzen betrachten. Aktionen sind in der App global, und ich denke, das ist in Ordnung. Ein Teil der App möchte aufgrund komplexer Produktanforderungen möglicherweise auf die Aktionen eines anderen Teils reagieren, und wir finden dies in Ordnung. Die Kopplung ist minimal: Alles, worauf Sie angewiesen sind, ist eine Schnur und die Form des Aktionsobjekts. Der Vorteil ist, dass es einfach ist, neue Ableitungen der Aktionen in verschiedenen Teilen der App einzuführen, ohne eine Menge Verbindungen mit Aktionserstellern herzustellen. Ihre Komponenten wissen nicht, was genau passiert, wenn eine Aktion ausgeführt wird – dies wird auf der Seite des Reduzierers entschieden.

Daher ist unsere offizielle Empfehlung, dass Sie zuerst versuchen sollten, verschiedene Reduzierstücke auf die gleichen Aktionen reagieren zu lassen. Wenn es unangenehm wird, erstellen Sie auf jeden Fall separate Aktionsersteller. Aber beginnen Sie nicht mit diesem Ansatz.

Wir empfehlen die Verwendung von Selektoren – tatsächlich empfehlen wir, Funktionen, die aus dem Zustand ("Selektoren") lesen, neben Reduzierern zu exportieren und sie immer in mapStateToProps anstatt die Zustandsstruktur in der Komponente fest zu kodieren. Auf diese Weise ist es einfach, die interne Zustandsform zu ändern. Sie können (müssen aber nicht) reselect für die Leistung verwenden, aber Sie können Selektoren auch naiv implementieren,

Vielleicht läuft es darauf hinaus, ob Sie im imperativen Stil oder im reaktiven Stil programmieren. Die Verwendung von Enten kann dazu führen, dass Aktionen und Reduzierer stark gekoppelt werden, was zwingendere Aktionen fördert.

  • Im imperativen Stil wird dem Laden vorgegeben, was zu tun ist, zB SHOW_MESSAGE_BOX oder SHOW_ERROR
  • Im reaktiven Stil wird dem Geschäft „eine Tatsache des Geschehens“ gegeben, zB DATA_FETCHING_FAILED oder USER_ENTERED_INVALID_THING_ID . Entsprechend reagiert der Laden.

Im vorherigen Beispiel habe ich keine SHOW_MESSAGE_BOX Aktion oder showError('Invalid thing id="'+id+'"') Aktionsersteller, weil das nicht der Wahrheit entspricht. Das ist ein Befehl.

Sobald diese Tatsache in den Laden gelangt, können Sie diese Tatsache in Befehle in Ihren reinen Reduzierern übersetzen, z

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

Wenn eine Aktion als „Fakt“ und nicht als „Befehl“ in den Laden gelangt, ist die Wahrscheinlichkeit geringer, dass sie schief geht, denn es ist eine Tatsache.

Wenn Ihre Reduzierstücke diese Tatsache falsch interpretiert haben, kann dies leicht behoben werden und die Korrektur kann durch die Zeit reisen. Wenn Ihre Aktionsersteller diese Tatsache jedoch falsch interpretieren, müssen Sie Ihre Aktionsersteller erneut ausführen.

Sie können Ihren Reduzierer auch so ändern, dass beim Auslösen von USER_ENTERED_INVALID_THING_ID das Textfeld für die Ding-ID zurückgesetzt wird. Und diese Veränderung reist auch durch die Zeit. Sie können Ihre Fehlermeldung auch lokalisieren, ohne die Seite zu aktualisieren. Das strafft die Feedbackschleife und macht das Debuggen und Optimieren viel einfacher.

(Ich spreche hier nur von den Vorteilen, natürlich gibt es Nachteile. Sie müssen viel mehr darüber nachdenken, wie Sie diese Tatsache darstellen können, da Ihr Geschäft nur synchron und ohne Nebenwirkungen auf diese Tatsachen reagieren kann. Siehe Diskussion zu alternative Async-/Nebenwirkungsmodelle und diese Frage habe ich auf StackOverflow gepostet . Ich denke, wir haben diesen Teil noch nicht genagelt.)


Ich bin mir ehrlich gesagt nicht sicher, wie ich anfangs über willkürliche Reduzierer denke, die auf eine Aktion reagieren. Es fühlt sich an wie eine schlechte Kapselung.

Es kommt auch häufig vor, dass mehrere Komponenten Daten aus demselben Speicher übernehmen. Es kommt auch häufig vor, dass eine einzelne Komponente von Daten aus mehreren Teilen des Speichers abhängt. Klingt das nicht auch ein bisschen nach schlechter Kapselung? Um wirklich modular zu werden, sollte nicht auch eine React-Komponente im "Duck"-Bundle enthalten sein? (Die Ulmenarchitektur tut das .)

React macht Ihre Benutzeroberfläche (daher der Name)

Auf die gleiche Weise glaube ich auch, dass Redux/Flux Ihr Datenmodell reaktiv macht , indem Aktionen als Tatsachen behandelt werden, sodass Sie Ihrem Datenmodell nicht sagen müssen, wie es sich selbst aktualisieren soll.

Vielen Dank, dass Sie sich die Zeit genommen haben, Ihre Gedanken aufzuschreiben und zu teilen, @dtinth. Danke auch @gaearon, dass hast . (Ich weiß, dass Sie eine Menge Zeug haben.) Sie beide haben mir einige zusätzliche Dinge gegeben, die ich berücksichtigen sollte. :)

Es kommt auch häufig vor, dass mehrere Komponenten Daten aus demselben Speicher übernehmen. Es kommt auch häufig vor, dass eine einzelne Komponente von Daten aus mehreren Teilen des Speichers abhängt. Klingt das nicht auch ein bisschen nach schlechter Kapselung?

Äh... einiges davon ist subjektiv, aber nein. Ich denke an die exportierten Aktionsersteller und Selektoren als API für das Modul.

Jedenfalls denke ich, dass dies eine gute Diskussion war. Wie Thai in seiner vorherigen Antwort erwähnt hat, gibt es Vor- und Nachteile dieser Ansätze, die wir diskutieren. Es war schön, Einblick in andere Ansätze zu bekommen. :)

Übrigens ist die Nachrichtenbox ein gutes Beispiel dafür, wo ich lieber einen separaten Aktionsersteller zum Anzeigen hätte. Vor allem, weil ich beim Erstellen eine Zeit verstreichen möchte, damit sie automatisch geschlossen werden kann (und bei der Aktionserstellung wird unreine Date.now() aufgerufen), weil ich einen Timer einrichten möchte, um sie zu schließen, ich möchten, dass dieser Timer usw. entprellt wird. Daher würde ich ein Meldungsfeld als den Fall betrachten, in dem sein "Aktionsfluss" wichtig genug ist, um seine persönlichen Aktionen zu rechtfertigen. Das sagte vielleicht, was ich beschrieben habe, kann mit https://github.com/yelouafi/redux-saga eleganter gelöst werden

Ich habe dies zunächst im Discord Reactiflux-Chat geschrieben, wurde aber gebeten, es hier einzufügen.

Ich habe in letzter Zeit oft über die gleichen Dinge nachgedacht. Ich habe das Gefühl, dass die Statusaktualisierungen in drei Teile unterteilt sind.

  1. Dem Aktionsersteller wird die Mindestmenge an Informationen übergeben, die zum Ausführen des Updates erforderlich ist. Dh alles, was aus dem aktuellen Zustand berechnet werden kann, sollte nicht darin enthalten sein.
  2. Der Status wird nach allen Informationen abgefragt, die Sie benötigen, um die Aktualisierungen durchzuführen (zB wenn Sie Todo mit der ID X kopieren möchten, holen Sie die Attribute von Todo mit der ID X ab, damit Sie eine Kopie erstellen können). Dies kann im Aktionsersteller erfolgen, und diese Informationen werden dann in das Aktionsobjekt aufgenommen. Dies führt zu fetten Aktionsobjekten. ODER es könnte im Reducer berechnet werden - dünne Aktionsobjekte.
  3. Basierend auf diesen Informationen wird eine reine Reduktionslogik angewendet, um den nächsten Zustand zu erhalten.

Das Problem ist nun, was in den Action-Creator und was in den Reducer eingefügt werden soll, die Wahl zwischen fetten und dünnen Action-Objekten. Wenn Sie die gesamte Logik in den Aktionsersteller stecken, erhalten Sie fette Aktionsobjekte, die im Grunde die Aktualisierungen des Zustands deklarieren. Reduzierer werden rein, dumm, fügen das hinzu, entfernen das, aktualisieren diese Funktionen. Sie werden leicht zu komponieren sein. Aber nicht viel von Ihrer Geschäftslogik wird dort sein.

Wenn Sie mehr Logik in den Reducer einfügen, erhalten Sie am Ende schöne, dünne Aktionsobjekte, die meisten Ihrer Datenlogik an einem Ort, aber Ihre Reducer sind schwieriger zu erstellen, da Sie möglicherweise Informationen aus anderen Zweigen benötigen. Am Ende haben Sie große Reduzierstücke oder Reduzierstücke, die zusätzliche Argumente von weiter oben im Staat übernehmen.

Ich weiß nicht, was die Antwort auf diese Probleme ist, bin mir nicht sicher, ob es noch eine gibt

Danke für das Teilen dieser Gedanken @tommikaikkonen. Ich bin selbst noch unschlüssig, was hier die "Antwort" ist. Deiner Zusammenfassung stimme ich zu. Ich würde dem Abschnitt "Alle Logik in den Aktionsersteller einfügen ..." eine kleine Anmerkung hinzufügen, nämlich, dass Sie (gemeinsame) Selektoren zum Lesen von Daten verwenden können, was in einigen Fällen nett sein kann.

Das ist ein interessanter Thread! Herauszufinden, wo der Code in einer Redux-App platziert werden soll, ist ein Problem, mit dem wir wohl alle konfrontiert sind. Ich mag die Idee von CQRS, einfach Dinge aufzuzeichnen, die passiert sind.
Aber ich kann hier eine Diskrepanz der Ideen sehen, weil in CQRS, AFAIK, die beste Vorgehensweise darin besteht, einen denormierten Zustand aus den Aktionen/Ereignissen zu erstellen, der von den Ansichten direkt konsumierbar ist. In Redux besteht die beste Vorgehensweise jedoch darin, einen vollständig normalisierten Zustand zu erstellen und die Daten Ihrer Ansichten über Selektoren abzuleiten.
Wenn wir einen denormalisierten Zustand erstellen, der von der Ansicht direkt konsumiert werden kann, verschwindet das Problem, dass ein Reducer Daten in einem anderen Reducer haben möchte (weil jeder Reducer einfach alle Daten speichern kann, die er benötigt, ohne sich um die Normalisierung zu kümmern). Aber dann bekommen wir andere Probleme beim Aktualisieren von Daten. Vielleicht ist das der Kern der Diskussion?

Aus vielen Jahren objektorientierter Entwicklung kommend. Redux fühlt sich wie ein großer Rückschritt an. Ich bettele selbst darum, Klassen zu erstellen, die Ereignisse (Aktionsersteller) und die Geschäftslogik kapseln. Ich versuche immer noch, einen sinnvollen Kompromiss zu finden, bin aber noch nicht in der Lage, dies zu tun. Geht es anderen ähnlich?

Objektorientierte Programmierung fördert das Zusammenfügen von Lese- und Schreibvorgängen. Dies macht eine Reihe von Dingen problematisch: Snapshotting und Rollback, zentralisierte Protokollierung, Debuggen von falschen Zustandsmutationen, granulare effiziente Updates. Wenn Sie nicht das Gefühl haben, dass dies die Probleme für Sie sind, wenn Sie wissen, wie Sie sie beim Schreiben von traditionellem objektorientiertem MVC-Code vermeiden können und wenn Redux mehr Probleme mit sich bringt, als es in Ihrer App löst, verwenden Sie Redux nicht :wink: .

@jayesbe Da Sie einen objektorientierten Programmierhintergrund haben, werden Sie möglicherweise feststellen, dass dies mit aufkommenden Ideen in der Programmierung kollidiert. Dies ist einer von vielen Artikeln zu diesem Thema: https://www.leaseweb.com/labs/2015/08/objektorientierte-programmierung-is-außergewöhnlich-bad/.

Durch die Trennung von Aktionen von der Datentransformation ist das Testen der Anwendung von Geschäftsregeln einfacher. Transformationen werden weniger abhängig vom Anwendungskontext.

Das bedeutet nicht, Objekte oder Klassen in Javascript aufzugeben. Beispielsweise werden React-Komponenten als Objekte und jetzt als Klassen implementiert. Aber React-Komponenten sind einfach darauf ausgelegt, eine Projektion der gelieferten Daten zu erstellen. Es wird empfohlen, reine Komponenten zu verwenden, die keinen Zustand speichern.

Hier kommt Redux ins Spiel: den Anwendungszustand zu organisieren und Aktionen und die entsprechende Transformation des Anwendungszustands zusammenzuführen.

@Johnsoftek danke für den Link. Jedoch basierend auf meiner Erfahrung in den letzten 10 Jahren.. Ich stimme dem nicht zu, aber wir müssen uns hier nicht auf die Debatte zwischen OO und Nicht-OO einlassen. Das Problem, das ich habe, ist die Organisation von Code und Abstraktion.

Mein Ziel ist es, eine einzelne App / einzelne Architektur zu erstellen, die (nur unter Verwendung von Konfigurationswerten) verwendet werden kann, um Hunderte von Apps zu erstellen. Der Anwendungsfall, mit dem ich mich befassen muss, ist der Umgang mit einer White-Label-Softwarelösung, die von vielen Kunden verwendet wird. Jeder nennt die Anwendung sein eigenes.

Ich habe einen, wie ich finde, interessanten Kompromiss gefunden... und ich denke, dass er gut genug damit umgeht, aber möglicherweise nicht die Standards der funktionalen Programmierung erfüllt. Ich würde es trotzdem gerne rausbringen.

Ich habe eine einzelne Anwendungsklasse, die mit der gesamten Geschäftslogik, API-Wrapper usw., die ich für die Schnittstelle zu meiner serverseitigen Anwendung benötige, in sich abgeschlossen ist.

Beispiel..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

Ich habe eine einzelne Instanz dieses Objekts erstellt und in den Kontext gestellt.. indem ich den Redux Provider erweitert habe

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

Dann habe ich diesen Anbieter als solchen genutzt

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

Endlich in meiner Komponente habe ich meine App verwendet..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

Der Action Creator ist also nur eine Callback-Funktion meiner Geschäftslogik.

                      app.login(value, actions.login);

Diese Lösung scheint im Moment gut zu funktionieren, obwohl ich nur mit der Authentifizierung begonnen habe.

Ich glaube, ich könnte den Store auch an meine Anwendungsinstanz übergeben, aber ich möchte das nicht, weil ich nicht möchte, dass der Store zufällige Mutationen erfährt. Der Zugriff auf den Store kann jedoch nützlich sein. Darüber werde ich mir bei Bedarf noch mehr Gedanken machen.

Ich habe einen, wie ich finde, interessanten Kompromiss gefunden... und ich denke, dass er gut genug damit umgeht, aber möglicherweise nicht die Standards der funktionalen Programmierung erfüllt.

Die „funktionale Masse“ wirst du hier nicht finden :wink: . Der Grund, warum wir in Redux funktionale Lösungen wählen, ist nicht, dass wir dogmatisch sind, sondern weil sie einige Probleme lösen, die Leute oft aufgrund von Klassen machen. Wenn wir beispielsweise Reduzierer von Aktionserstellern trennen, können wir Lese- und Schreibvorgänge voneinander trennen, was für das Protokollieren und Reproduzieren von Fehlern wichtig ist. Aktionen, die einfache Objekte sind, ermöglichen das Aufzeichnen und Wiedergeben, da sie serialisierbar sind. In ähnlicher Weise macht es es sehr einfach, state auf dem Server zu serialisieren und auf dem Client für das Server-Rendering zu deserialisieren oder Teile davon in localStorage beizubehalten, da es sich um ein einfaches Objekt und nicht um eine Instanz von MyAppState localStorage . Das Ausdrücken von Reduzierern als Funktionen ermöglicht es uns, Zeitreisen und Hot-Reloading zu implementieren, und das Ausdrücken von Selektoren als Funktionen erleichtert das Hinzufügen von Memos. All diese Vorteile haben nichts damit zu tun, dass wir eine „funktionale Masse“ sind, sondern haben alles mit der Lösung bestimmter Aufgaben zu tun, für deren Lösung diese Bibliothek geschaffen wurde.

Ich habe eine einzelne Instanz dieses Objekts erstellt und in den Kontext gestellt.. indem ich den Redux Provider erweitert habe

Das sieht für mich total vernünftig aus. Wir haben keinen irrationalen Hass auf Klassen. Der Punkt ist, dass wir sie lieber nicht in Fällen verwenden möchten, in denen sie stark einschränken (z. B. für Reducer oder Aktionsobjekte), aber es ist in Ordnung, sie beispielsweise zum Generieren von Aktionsobjekten zu verwenden.

Ich würde jedoch vermeiden, Provider da dies zerbrechlich ist. Es ist nicht erforderlich: React führt den Kontext von Komponenten zusammen, sodass Sie ihn stattdessen einfach umhüllen können.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

Es liest sich meiner Meinung nach tatsächlich einfacher und ist weniger zerbrechlich.

Alles in allem macht Ihr Ansatz also absolut Sinn. Die Verwendung einer Klasse unterscheidet sich in diesem Fall nicht wirklich von etwas wie createActions(config) , einem Muster, das wir auch empfehlen, wenn Sie die Aktionsersteller parametrisieren müssen. Daran ist absolut nichts auszusetzen.

Wir raten Ihnen nur davon ab, Klasseninstanzen für Zustands- und Aktionsobjekte zu verwenden, da Klasseninstanzen die Serialisierung sehr schwierig machen. Für Reduzierer empfehlen wir auch nicht, Klassen zu verwenden, da es schwieriger ist, die Reduzierer-Zusammensetzung zu verwenden, d. h. Reduzierer, die andere Reduzierer aufrufen. Für alles andere können Sie jedes Mittel zur Codeorganisation verwenden, einschließlich Klassen.

Wenn Ihre Anwendung und Konfiguration unveränderlich sind (und ich denke, sie sollten es sein, aber vielleicht habe ich zu viel funktionale Kühlhilfe getrunken), dann könnten Sie den folgenden Ansatz in Betracht ziehen:

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

Und dann in mapStateToProps:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

Dann brauchen Sie nicht die von Ihnen übernommene Provider-Technik, sondern holen sich nur das Anwendungsobjekt vom Staat. Dank reselect wird das Application-Objekt nur erstellt, wenn sich die Konfiguration ändert, was wahrscheinlich nur einmal der Fall ist.

Wo ich denke, dass dieser Ansatz einen Vorteil haben kann, ist, dass Sie die Idee leicht erweitern können, um mehrere solcher Objekte zu haben und diese Objekte auch von anderen Teilen des Staates abhängen zu lassen. Sie könnten beispielsweise eine UserControl-Klasse mit Login/Logout/etc-Methoden haben, die sowohl auf die Konfiguration als auch auf einen Teil Ihres Status zugreifen kann.

Alles in allem macht Ihr Ansatz also absolut Sinn.

:+1: Danke das hilft. Ich stimme der Verbesserung auf MyProvider zu. Ich werde meinen Code aktualisieren, um zu folgen. Eines der größten Probleme, die ich hatte, als ich Redux zum ersten Mal lernte, war der semantische Begriff von "Action Creators". Für mich war es eine Art Erkenntnis, das war wie.. das sind Ereignisse, die gesendet werden.

@winstonewert ist createSelector auf

Eines der größten Probleme, die ich hatte, als ich Redux zum ersten Mal lernte, war der semantische Begriff von "Action Creators". Für mich war es eine Art Erkenntnis, das war wie.. das sind Ereignisse, die gesendet werden.

Ich würde sagen, dass es in Redux überhaupt keinen semantischen Begriff von Action Creators gibt. Es gibt einen semantischen Begriff von Aktionen (die beschreiben, was passiert ist und ungefähr Ereignissen entsprechen, aber nicht ganz – siehe zB Diskussion in #351). Aktionsersteller sind nur ein Muster zum Organisieren des Codes. Es ist praktisch, Fabriken für Aktionen zu haben, weil Sie sicherstellen möchten, dass Aktionen desselben Typs eine konsistente Struktur haben, dieselben Nebeneffekte haben, bevor sie gesendet werden usw. Aber aus Sicht von Redux gibt es keine Aktionsersteller – Redux sieht nur Aktionen.

ist createSelector auf React-native verfügbar?

Es ist in Reselect verfügbar, das

Ahh. OK habe es. Die Dinge sind viel klarer. Danke schön.

Verschachteln Sie keine Objekte in mapStateToProps und mapDispatchToProps

Ich bin kürzlich auf ein Problem gestoßen, bei dem verschachtelte Objekte verloren gingen, wenn Redux mapStateToProps und mapDispatchToProps zusammenführt (siehe Reactjs/react-redux#324). Obwohl @gaearon eine Lösung bietet, die es mir ermöglicht, verschachtelte Objekte zu verwenden, sagt er weiter, dass dies ein Anti-Muster ist:

Beachten Sie, dass das Gruppieren von Objekten wie diesem unnötige Zuweisungen verursacht und auch Leistungsoptimierungen erschwert, da wir uns nicht mehr auf die flache Gleichheit von Ergebniseigenschaften verlassen können, um festzustellen, ob sie sich geändert haben. Sie werden also mehr Renderings sehen als bei einem einfachen Ansatz ohne Namespace, den wir in den Dokumenten empfehlen.

@bvaughn sagte

Reduzierstücke sollten dumm und einfach sein

und wir sollten die meisten Geschäftslogiken in Aktionsersteller umsetzen, dem stimme ich absolut zu. Aber wenn alles in Aktion getreten ist, warum müssen wir dann noch Reduzierdateien und Funktionen manuell erstellen? Warum nicht die Daten, die in Aktionen verarbeitet wurden, direkt speichern?

Es hat mich eine Zeitlang verwirrt...

warum müssen wir Reduzierdateien und Funktionen immer noch manuell erstellen?

Da Reduzierer reine Funktionen sind, können Sie den Reduzierer bei einem Fehler in der Statusaktualisierungslogik im laufenden Betrieb neu laden. Das Entwicklungstool kann dann die App in den Anfangszustand zurückspulen und dann alle Aktionen mit dem neuen Reducer wiederholen. Dies bedeutet, dass Sie Statusaktualisierungsfehler beheben können, ohne die Aktion manuell zurücksetzen und erneut ausführen zu müssen. Dies ist der Vorteil, den Großteil der Zustandsaktualisierungslogik im Reduzierer zu behalten.

Dies ist der Vorteil, den Großteil der Zustandsaktualisierungslogik im Reduzierer zu behalten.

@dtinth Nur zur Verdeutlichung, wenn Sie "Logik zur

Ich bin mir nicht so sicher, ob es eine gute Idee ist, den größten Teil Ihrer Logik in Aktionsersteller zu stecken. Wenn alle Ihre Reducer nur triviale Funktionen sind, die ADD_X -ähnliche Aktionen akzeptieren, dann ist es unwahrscheinlich, dass sie Fehler haben - großartig! Aber dann wurden alle Ihre Fehler an Ihre Action-Ersteller weitergegeben und Sie verlieren die großartige Debugging-Erfahrung, auf die @dtinth anspielt.

Aber auch wie @tommikaikkonen erwähnt hat, ist es nicht so einfach, komplexe Reduzierer zu schreiben. Mein Bauchgefühl ist jedoch, dass Sie dort ansetzen möchten, wenn Sie die Vorteile von Redux nutzen möchten - ansonsten drängen Sie Ihre reinen Funktionen, um nur die trivialsten Aufgaben zu bewältigen, anstatt die Nebenwirkungen an den Rand zu drängen, und lassen die meisten übrig Ihrer App in der staatsgeplagten Hölle. :)

@sompylasar "Geschäftslogik" und "Zustandsaktualisierungslogik" sind imo irgendwie dasselbe.

Um mich jedoch mit meinen eigenen Implementierungsspezifika zu verbinden.. meine Aktionen sind in erster Linie Nachschlagen von Eingaben in die Aktion. Eigentlich sind alle meine Aktionen rein, da ich meine gesamte "Geschäftslogik" in einen Anwendungskontext verschoben habe.

als Beispiel.. das ist mein typischer Reduzierer

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

Meine typischen Aktionen:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

Meine Geschäftslogik ist in einem im Kontext gespeicherten Objekt definiert, d.

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

Wenn Sie meinen obigen Kommentar sehen, hatte ich auch Probleme mit dem besten Ansatz. Alle Aktionen, die meiner Anwendung bekannt sind, werden hier zugeordnet.

Ich verwende die mapDispatchToProps() nirgendwo mehr. Für Redux habe ich jetzt nur mapStateToProps beim Erstellen einer verbundenen Komponente. Wenn ich Aktionen auslösen muss.. kann ich sie über mein Anwendungsobjekt über den Kontext auslösen.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

Die obige Komponente muss nicht mit Redux verbunden sein. Es kann weiterhin Aktionen ausführen. Wenn Sie den Zustand innerhalb der Komponente aktualisieren müssen, würden Sie sie natürlich in eine verbundene Komponente umwandeln, um sicherzustellen, dass die Zustandsänderung weitergegeben wird.

So habe ich meine Kern-"Geschäftslogik" organisiert. Da mein Zustand wirklich auf einem Backend-Server gepflegt wird, funktioniert dies für meinen Anwendungsfall wirklich gut.

Wo Sie Ihre "Geschäftslogik" speichern, liegt ganz bei Ihnen und wie sie zu Ihrem Anwendungsfall passt.

@jayesbe Der folgende Teil bedeutet, dass Sie keine "Geschäftslogik" in den Reducern haben, und außerdem ist die Zustandsstruktur in die Action-Ersteller verschoben, die die Nutzlast erstellen, die über den Reducer in den Store übertragen wird:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe Meine Aktionen und Reduzierungen sind Ihren sehr ähnlich, einige Aktionen erhalten ein einfaches Netzwerkantwortobjekt als Argument, und ich habe die Logik gekapselt, wie die Antwortdaten in der Aktion verarbeitet werden und schließlich ein sehr einfaches Objekt als Rückgabewert zurückgegeben und dann an übergeben werden Reduzierer über Call Dispatch(). So wie du es getan hast. Das Problem ist, wenn Ihre Aktion auf diese Weise geschrieben wurde, Ihre Aktion fast alles getan hat und die Verantwortung Ihres Reducers sehr gering ist, warum müssen wir die Daten manuell zum Speichern übertragen, wenn der Reducer das Aktionsobjekt einfach nur verteilt? Redux dosieren Sie es automatisch für uns ist überhaupt keine schwierige Sache.

Nicht unbedingt. Aber oft ein Teil des Geschäftsprozesses
beinhaltet die Aktualisierung des Anwendungsstatus gemäß den Geschäftsregeln,
Daher müssen Sie möglicherweise eine Geschäftslogik einfügen.

Für einen extremen Fall, überprüfen Sie dies:
„Synchrone RTS-Engines und eine Geschichte von Desyncs“ @ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

Am 5. April 2016 um 17:54 Uhr schrieb "John Babak" [email protected] :

Dies ist der Vorteil, den Großteil der Statusaktualisierungslogik im
Reduzierer.

@dtinth https://github.com/dtinth Nur zur Verdeutlichung, indem man sagt
"Zustandsaktualisierungslogik" meinst du "Geschäftslogik"?


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki Der Rat, Ihre Reduzierer einfach zu halten und gleichzeitig komplexe Logik in Aktionserstellern

Aus diesem Grund überträgt Redux Daten von Aktionen nicht automatisch an den Store. Denn so sollten Sie Redux nicht verwenden. Redux wird nicht geändert, um eine andere als die beabsichtigte Verwendung zu ermöglichen. Natürlich steht es Ihnen völlig frei, das zu tun, was für Sie funktioniert, aber erwarten Sie nicht, dass Redux sich dafür ändert.

Für was es wert ist, produziere ich meine Reduzierstücke mit etwas wie:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions gibt sowohl den Reducer als auch die Action Creators zurück. Auf diese Weise finde ich es sehr einfach, meine Update-Logik im Reducer zu behalten, ohne viel Zeit damit verbringen zu müssen, triviale Action-Ersteller zu schreiben.

Wenn Sie darauf bestehen, Ihre Logik in Ihren Aktionserstellern beizubehalten, sollten Sie keine Probleme haben, die Daten selbst zu automatisieren. Ihr Reduzierstück ist eine Funktion, und es kann tun, was es will. Ihr Reduzierstück könnte also so einfach sein wie:

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

Ausgehend von den vorherigen Punkten zur Geschäftslogik denke ich, dass Sie Ihre Geschäftslogik in zwei Teile unterteilen können:

  • Der nichtdeterministische Teil. Dieser Teil verwendet externe Dienste, asynchronen Code, Systemzeit oder einen Zufallszahlengenerator. Meiner Meinung nach wird dieser Teil am besten von einer I/O-Funktion (oder einem Aktionsersteller) gehandhabt. Ich nenne sie den Geschäftsprozess.

Stellen Sie sich eine IDE-ähnliche Software vor, bei der der Benutzer auf die Schaltfläche Ausführen klicken kann und die App kompiliert und ausgeführt wird. (Hier verwende ich eine asynchrone Funktion, die einen Speicher benötigt, aber Sie können stattdessen redux-thunk verwenden.)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • Der deterministische Teil. Dieser Teil hat ein völlig vorhersehbares Ergebnis. Bei gleichem Zustand und gleichem Ereignis ist das Ergebnis immer vorhersehbar. Meiner Meinung nach wird dieser Teil am besten mit einem Reduzierstück behandelt. Ich nenne das die Geschäftsregeln.

``` js
importiere dich von 'updeep'

export const Reducer = createReducer({
// [Aktionsname]: Aktion => currentState => nextState
startCompiling: () => u({ compiling: true }),
errorCompiling: ({ error }) => u({ compiling: false, compileError: error }),
startRunning: ({ app }) => u({
läuft: () => App,
Kompilieren: falsch
}),
stopRunning: () => u({ running: false }),
discardCompileError: () => u({ compileError: null }),
// ...
})
```

Ich versuche, so viel Code wie möglich in dieses deterministische Land zu stecken, wobei die alleinige Verantwortung des Reducers darin besteht, den Anwendungsstatus angesichts der eingehenden Aktionen konsistent zu halten. Nichts mehr. Alles andere würde ich außerhalb von Redux machen, da Redux nur ein Zustandscontainer ist.

@dtinth Toll, denn das vorherige Beispiel in https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 sieht ganz anders aus als das, was du in https://github.com/reactjs/redux/ geschrieben hast.

@winstonewert

Das von Redux empfohlene Muster ist das Gegenteil: Halten Sie die Ersteller von Aktionen einfach, während Sie die komplexe Logik in den Reduzierern beibehalten.

Wie kann man komplexe Logik in die Ruducer einbauen und sie trotzdem rein halten?

Wenn ich zum Beispiel fetch() aufrufe und Daten vom Server lade.. dann irgendwie verarbeiten. Ich habe noch kein Beispiel für einen Reduzierer mit "komplexer Logik" gesehen

@jayesbe : Äh... "komplex" und "rein" sind orthogonal. Sie können _wirklich_ komplizierte bedingte Logik oder Manipulation in einem Reduzierer haben, und solange es nur eine Funktion seiner Eingaben ohne Nebenwirkungen ist, ist es immer noch rein.

Wenn Sie einen komplexen lokalen Status haben (denken Sie an einen Post-Editor, eine Baumansicht usw.) oder Dinge wie optimistische Updates handhaben, enthalten Ihre Reduzierer eine komplexe Logik. Es hängt wirklich von der App ab. Einige haben komplexe Anfragen, andere haben komplexe Statusaktualisierungen. Manche haben beides :-)

@markerikson ok logische Anweisungen sind eine Sache.. aber bestimmte Aufgaben ausführen? Ich habe beispielsweise eine Aktion, die in einem Fall drei andere Aktionen auslöst oder in einem anderen Fall zwei verschiedene und separate Aktionen auslöst. Diese Logik + Ausführung von Aufgaben hört sich nicht so an, als ob sie in Reduzierer gehen sollten.

Mein Zustandsdaten-/Modellzustand befindet sich auf dem Server, der Ansichtszustand unterscheidet sich vom Datenmodell, aber die Verwaltung dieses Zustands liegt auf dem Client. Mein Datenmodellstatus wird einfach an die Ansicht weitergegeben. Das macht meine Reduzierer und Aktionen so schlank.

@jayesbe : Ich glaube nicht, dass jemals jemand gesagt hat, dass das Auslösen anderer Aktionen in einem Reduzierer erfolgen sollte. Und eigentlich sollte es nicht. Die Aufgabe eines Reduzierers ist einfach (currentState + action) -> newState .

Wenn Sie mehrere Aktionen miteinander verknüpfen müssen, tun Sie dies entweder in einem Thunk oder einer Saga und feuern sie nacheinander ab, oder lassen Sie etwas auf Zustandsänderungen hören oder verwenden Sie eine Middleware, um eine Aktion abzufangen und zusätzliche Arbeit zu erledigen.

Ich bin etwas verwirrt, was die Diskussion an dieser Stelle ist, um ehrlich zu sein.

@markerikson das Thema scheint sich um die "Geschäftslogik" zu

Wie Sie feststellen, ist nur das, was für Sie funktioniert, wirklich wichtig. Aber so würde ich die Probleme angehen, nach denen Sie gefragt haben.

Wenn ich zum Beispiel fetch() aufrufe und Daten vom Server lade.. dann irgendwie verarbeiten. Ich habe noch kein Beispiel für einen Reduzierer mit "komplexer Logik" gesehen

Mein Reducer nimmt eine Rohantwort von meinem Server und aktualisiert meinen Zustand damit. Auf diese Weise erfolgt die Verarbeitungsantwort, von der Sie sprechen, in meinem Reduzierer. Die Anforderung könnte beispielsweise das Abrufen von JSON-Datensätzen für meinen Server sein, die der Reducer in meinem lokalen Datensatz-Cache speichert.

k logische Anweisungen sind eine Sache... aber bestimmte Aufgaben ausführen? Ich habe beispielsweise eine Aktion, die in einem Fall drei andere Aktionen auslöst oder in einem anderen Fall zwei verschiedene und separate Aktionen auslöst. Diese Logik + Ausführung von Aufgaben hört sich nicht so an, als ob sie in Reduzierer gehen sollten.

Das hängt davon ab, was Sie tun. Offensichtlich wird im Fall des Serverabrufs eine Aktion eine andere auslösen. Das entspricht vollständig der empfohlenen Redux-Prozedur. Sie können jedoch auch so etwas tun:

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

Dies ist nicht die empfohlene Methode zur Verwendung von Redux. Die empfohlene Redux-Methode besteht darin, eine einzige CREATE_FOOBAR-Aktion zu verwenden, die alle diese gewünschten Änderungen bewirkt.

@winstonewert :

Dies ist nicht die empfohlene Methode zur Verwendung von Redux. Die empfohlene Redux-Methode besteht darin, eine einzige CREATE_FOOBAR-Aktion zu verwenden, die alle diese gewünschten Änderungen bewirkt.

Haben Sie einen Hinweis auf eine angegebene Stelle? Denn als ich für die FAQ-Seite recherchierte, kam ich auf "es kommt darauf an", direkt von Dan. Siehe http://redux.js.org/docs/FAQ.html#actions -multiple-actions und diese Antwort von Dan auf SO .

"Geschäftslogik" ist wirklich ein ziemlich weit gefasster Begriff. Es kann Dinge umfassen wie "Ist etwas passiert?", "Was machen wir jetzt, da dies passiert ist?", "Ist das gültig?" und so weiter. Basierend auf dem Design von Redux _können_ diese Fragen je nach Situation an verschiedenen Stellen beantwortet werden, obwohl ich "ist es passiert" eher als eine Verantwortung des Aktionserstellers und "was jetzt" fast definitiv als eine Reduziererverantwortung ansehen würde.

Insgesamt ist meine Meinung zu dieser _gesamten Frage_ der "Geschäftslogik": _"es kommt darauf an _". Es gibt Gründe, warum Sie das Analysieren von Anforderungen in einem Aktionsersteller durchführen möchten, und Gründe, warum Sie dies möglicherweise in einem Reduzierer tun möchten. Es gibt Zeiten, in denen Ihr Reduzierer einfach "nimm dieses Objekt und schlage es in meinen Zustand" und andere Zeiten, in denen dein Reduzierer eine sehr komplexe bedingte Logik sein könnte. Es gibt Zeiten, in denen Ihr Action-Creator sehr einfach sein kann, und andere Zeiten, in denen er komplex sein kann. Manchmal ist es sinnvoll, mehrere Aktionen hintereinander auszuführen, um Schritte eines Prozesses darzustellen, und manchmal möchten Sie nur eine generische Aktion "THING_HAPPENED" ausführen, um alles darzustellen.

Die einzige feste Regel, der ich zustimmen würde, ist "Nicht-Determinismus bei Aktionserstellern, reiner Determinismus bei Reduzierern". Das ist eine gegebene.

Abgesehen davon? Finden Sie etwas, das für Sie funktioniert. Seien Sie konsequent. Wissen Sie, warum Sie es auf eine bestimmte Weise tun. Gehen Sie damit.

obwohl ich "ist es passiert" eher als eine Verantwortung des Aktionserstellers sehen würde und "was jetzt" fast definitiv eine Reduziererverantwortung ist.

Deshalb gibt es eine parallele Diskussion, wie man Nebeneffekte, dh den nicht reinen Teil des "Was jetzt", in Reducer steckt: #1528 und sie zu reinen Beschreibungen dessen macht, was passieren soll, wie die nächsten Aktionen, die ausgelöst werden sollen.

Das Muster, das ich verwendet habe, ist:

  • Was zu tun ist : Aktion/Aktionsersteller
  • How To Do It: Middleware (z. B. Middleware , die Streams für ‚Asynchron‘ Aktionen und Anrufe an meinem API - Objekt macht)
  • Was ist mit dem Ergebnis zu tun : Reduzierer

Von früher in diesem Thread lautete Dans Aussage:

Daher ist unsere offizielle Empfehlung, dass Sie zuerst versuchen sollten, verschiedene Reduzierstücke auf die gleichen Aktionen reagieren zu lassen. Wenn es unangenehm wird, erstellen Sie auf jeden Fall separate Aktionsersteller. Aber beginnen Sie nicht mit diesem Ansatz.

Daher gehe ich davon aus, dass der empfohlene Ansatz darin besteht, eine Aktion pro Ereignis auszulösen. Aber tun Sie pragmatisch, was funktioniert.

@winstonewert : Dan bezieht sich auf das Muster der "Reduziererzusammensetzung", dh "ist eine Aktion, die immer nur von einem Reduzierer gehört wird" vs "viele Reduzierer können auf dieselbe Aktion reagieren". Dan legt großen Wert auf willkürliche Reduzierer, die auf eine einzige Aktion reagieren. Andere bevorzugen Dinge wie den "Enten"-Ansatz, bei dem Reduzierer und Aktionen SEHR eng gebündelt sind und nur ein Reduzierer jemals eine bestimmte Aktion behandelt. In diesem Beispiel geht es also nicht darum, "mehrere Aktionen nacheinander auszulösen", sondern "wie viele Teile meiner Reduziererstruktur erwarten, darauf zu reagieren".

Aber tun Sie pragmatisch, was funktioniert.

:+1:

@sompylasar Ich sehe den Fehler meiner Art, indem ich die Zustandsstruktur in meinen Aktionen habe. Ich kann die Zustandsstruktur leicht in meine Reduzierer verschieben und meine Aktionen vereinfachen. Danke schön.

Mir scheint, dass es dasselbe ist.

Entweder haben Sie eine einzelne Aktion, die mehrere Reduzierer auslöst, die mehrere Zustandsänderungen verursachen, oder Sie haben mehrere Aktionen, die jeweils einen einzelnen Reduzierer auslösen, der eine einzelne Zustandsänderung verursacht. Wenn mehrere Reduzierer auf eine Aktion reagieren und eine Ereigniszuteilung mehrere Aktionen haben, sind alternative Lösungen für dasselbe Problem.

In der von Ihnen erwähnten StackOverflow-Frage sagt er:

Führen Sie das Aktionsprotokoll so nah wie möglich am Verlauf der Benutzerinteraktionen. Wenn es jedoch schwierig ist, Reduzierer zu implementieren, sollten Sie einige Aktionen in mehrere aufteilen, wenn ein UI-Update als zwei separate Operationen angesehen werden kann, die zufällig zusammen sind.

Aus meiner Sicht empfiehlt Dan, eine Aktion pro Benutzerinteraktion aufrechtzuerhalten, als idealen Weg. Aber er ist pragmatisch, wenn es schwierig wird, den Reduzierer zu implementieren, befürwortet er die Aufteilung der Aktion.

Ich visualisiere hier ein paar ähnliche, aber etwas unterschiedliche Anwendungsfälle:

1) Eine Aktion erfordert Aktualisierungen in mehreren Bereichen Ihres Bundesstaates, insbesondere wenn Sie combineReducers , um separate Reduzierfunktionen für jede Unterdomäne zu verwenden. Tust du:

  • Lassen Sie sowohl Reducer A als auch Reducer B auf dieselbe Aktion reagieren und ihre Zustandsbits unabhängig voneinander aktualisieren
  • Lassen Sie Ihren Thunk ALLE relevanten Daten in die Aktion stopfen, damit jeder Reduzierer auf andere Zustandsbits außerhalb seines eigenen Chunks zugreifen kann
  • einen weiteren Reduzierer der obersten Ebene hinzufügen, der Teile des Zustands von Reduzierer A erfasst und ihn im Sonderfall an Reduzierer B weitergibt?
  • eine für Reduzierer A bestimmte Aktion mit den benötigten Zustandsbits und eine zweite Aktion für Reduzierer B mit dem, was er benötigt, abfertigen?

2) Sie haben eine Reihe von Schritten, die in einer bestimmten Reihenfolge ausgeführt werden, wobei jeder Schritt eine Statusaktualisierung oder Middleware-Aktion erfordert. Tust du:

  • Jeden einzelnen Schritt als separate Aktion versenden, um den Fortschritt darzustellen, und einzelne Reduzierer auf bestimmte Aktionen reagieren lassen?
  • Ein großes Ereignis absetzen und darauf vertrauen, dass die Reduzierer es angemessen handhaben?

Also ja, definitiv einige Überschneidungen, aber ich denke, dass ein Teil des Unterschieds im mentalen Bild hier die verschiedenen Anwendungsfälle sind.

@markerikson Ihr Rat ist also: "Es hängt davon ab, in welcher Situation Sie sich befinden", und wie Sie die "Geschäftslogik" auf Aktionen oder Reduzierungen ausbalancieren können, liegt in Ihrer Überlegung. Wir sollten auch die Vorteile der reinen Funktion so weit wie möglich nutzen?

Ja. Reduzierstücke _müssen_ rein sein, als Redux-Anforderung (außer in 0,00001 % der Sonderfälle). Action-Schöpfer müssen absolut _nicht _ rein sein, und in der Tat werden die meisten Ihrer "Unreinheiten" leben. Da reine Funktionen jedoch offensichtlich einfacher zu verstehen und zu testen sind als unreine Funktionen, _wenn_ Sie einen Teil Ihrer Logik zur Aktionserstellung rein machen können, großartig! Wenn nicht, ist das in Ordnung.

Und ja, aus meiner Sicht liegt es an Ihnen als Entwickler, zu bestimmen, was eine angemessene Balance für die Logik Ihrer eigenen App ist und wo sie lebt. Es gibt keine einzige feste Regel, für welche Seite der Trennung zwischen Action Creator und Reducer es leben sollte. (Ähm, mit Ausnahme der oben erwähnten Sache "Determinismus / Nicht-Determinismus". Auf die ich in diesem Kommentar eindeutig Bezug nehmen wollte. Offensichtlich.)

@cpsubrian

Was tun mit Ergebnis: Reduzierer

Eigentlich sind Sagen dafür da: um mit Effekten umzugehen wie "Wenn das passiert ist, dann sollte das auch passieren"


@markerikson @LumiaSaki

Action-Schöpfer müssen absolut nicht rein sein, und in der Tat werden die meisten deiner "Unreinheiten" leben.

Tatsächlich müssen Action-Schöpfer nicht einmal unrein sein oder überhaupt existieren.
Siehe http://stackoverflow.com/a/34623840/82609

Und ja, aus meiner Sicht liegt es an Ihnen als Entwickler, zu bestimmen, was eine angemessene Balance für die Logik Ihrer eigenen App ist und wo sie lebt. Es gibt keine einzige feste Regel, für welche Seite der Trennung zwischen Action Creator und Reducer es leben sollte.

Ja, aber es ist nicht so offensichtlich, die Nachteile jedes Ansatzes ohne Erfahrung zu bemerken :) Siehe auch meinen Kommentar hier: https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

Für die meisten einfachen Apps funktioniert keine strenge Regel gut, aber wenn Sie wiederverwendbare Komponenten erstellen möchten, sollten diese Komponenten nichts außerhalb ihres eigenen Geltungsbereichs erkennen.

Anstatt also eine globale Aktionsliste für Ihre gesamte App zu definieren, können Sie Ihre App in wiederverwendbare Komponenten aufteilen und jede Komponente hat ihre eigene Aktionsliste und kann diese nur versenden/reduzieren. Das Problem ist dann, wie Sie ausdrücken "Wenn das Datum in meiner Datumsauswahl ausgewählt wurde, sollten wir eine Erinnerung für dieses Aufgabenelement speichern, einen Feedback-Toast anzeigen und dann die App mit Erinnerungen zu den Aufgaben navigieren": Hier saga kommt zum Einsatz: Orchestrierung der Komponenten

Siehe auch https://github.com/slorber/scalable-frontend-with-elm-or-redux

Und ja, aus meiner Sicht liegt es an Ihnen als Entwickler, zu bestimmen, was eine angemessene Balance für die Logik Ihrer eigenen App ist und wo sie lebt. Es gibt keine einzige feste Regel, für welche Seite der Trennung zwischen Action Creator und Reducer es leben sollte.

Ja, es gibt keine Anforderung von Redux, ob Sie Ihre Logik in die Reducer oder Action Creators einsetzen. Redux wird so oder so nicht kaputt gehen. Es gibt keine feste Regel, die von Ihnen verlangt, es auf die eine oder andere Weise zu tun. Aber Dans Empfehlung lautete: "Führen Sie das Aktionsprotokoll so nah wie möglich an den Verlauf der Benutzerinteraktionen." Das Auslösen einer einzelnen Aktion pro Benutzerereignis ist nicht erforderlich, wird jedoch empfohlen.

In meinem Fall habe ich 2 Reduzierstücke, die an 1 Aktion interessiert sind. Die rohen action.data reichen nicht aus. Sie müssen mit transformierten Daten umgehen. Ich wollte die Transformation nicht in den 2 Reduzierern durchführen. Also habe ich die Funktion zum Durchführen der Transformation in einen Thunk verschoben. So erhalten meine Reduzierstücke verbrauchsfertige Daten. Dies ist das Beste, was ich in meiner kurzen 1-monatigen Redux-Erfahrung denken konnte.

Wie wäre es mit der Entkopplung der Komponenten/Ansichten von der Struktur des Stores? Mein Ziel ist es, dass alles, was von der Struktur des Stores betroffen ist, in den Reducern verwaltet werden sollte, deshalb verwende ich gerne Selektoren mit Reducern, damit Komponenten nicht wirklich wissen müssen, wie sie einen bestimmten Knoten des Stores erhalten.

Das ist großartig, um Daten an die Komponenten zu übergeben, was ist umgekehrt, wenn Komponenten Aktionen ausführen:

Nehmen wir zum Beispiel an, dass ich in einer Todo-App den Namen eines Todo-Elements aktualisiere, also sende ich eine Aktion, die den Teil des Elements übergibt, den ich aktualisieren möchte, dh:

dispatch(updateItem({name: <text variable>}));

, und die Aktionsdefinition lautet:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

was wiederum vom Reduzierstück gehandhabt wird, das einfach tun könnte:

Object.assign({}, item, action.updatedData)

um den Artikel zu aktualisieren.

Dies funktioniert hervorragend, da ich dieselbe Aktion und denselben Reduzierer wiederverwenden kann, um jede Requisite des Todo-Gegenstands zu aktualisieren, dh:

updateItem({description: <text variable>})

wenn stattdessen die Beschreibung geändert wird.

Aber hier muss die Komponente wissen, wie ein Todo-Element im Store definiert ist, und wenn sich diese Definition ändert, muss ich daran denken, sie in allen davon abhängigen Komponenten zu ändern, was offensichtlich eine schlechte Idee ist, irgendwelche Vorschläge für dieses Szenario?

@dcoellarb

Meine Lösung in dieser Art von Situation besteht darin, die Flexibilität von Javascript zu nutzen, um das zu generieren, was Boilerplate wäre.

Also ich könnte haben:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Dabei ist makeRecord eine Funktion zum automatischen Erstellen von Reduzierern, Aktionserstellern und Selektoren aus meiner Beschreibung. Dadurch entfällt die Boilerplate, aber wenn ich später etwas tun muss, das nicht in dieses ordentliche Muster passt, kann ich dem Ergebnis von makeRecord benutzerdefinierte Reduzierer / Aktionen / Selektoren hinzufügen.

tks @winstonewert ich mag den Ansatz, den Boilerplate zu vermeiden, ich kann sehen, dass in Apps mit vielen Modellen viel Zeit gespart wird; aber ich sehe immer noch nicht, wie dies die Komponente von der Store-Struktur entkoppelt. Ich meine, selbst wenn die Aktion generiert wird, muss die Komponente immer noch die aktualisierten Felder an sie übergeben, was bedeutet, dass die Komponente noch die Struktur von kennen muss der Laden richtig?

@winstonewert @dcoellarb Meiner Meinung nach sollte die Aktionsnutzlaststruktur zu Aktionen gehören, nicht zu Reducern, und in einem Reducer explizit in eine Zustandsstruktur übersetzt werden. Es sollte ein glücklicher Zufall sein, dass sich diese Strukturen der Einfachheit halber spiegeln. Diese beiden Strukturen müssen sich nicht immer spiegeln, da sie nicht dieselbe Einheit sind.

@sompylasar richtig, ich mache das, ich übersetze die api/rest-Daten in meine Store-Struktur, aber immer noch sollten die Reduzierer und die Selektoren die einzige, die die Store-Struktur kennen sollte, richtig? Der Grund, warum ich sie zusammenfüge, ist mein Problem mit den Komponenten / Ansichten. Ich würde es vorziehen, dass sie die Store-Struktur nicht kennen müssen, falls ich sie später ändern möchte, aber wie in meinem Beispiel erklärt, müssen sie die Struktur kennen, damit sie es können Senden Sie die richtigen Daten, um aktualisiert zu werden, ich habe keinen besseren Weg gefunden, dies zu tun :(.

@dcoellarb Sie können sich Ihre Ansichten als Eingaben von Daten eines bestimmten Typs

@sompylasar macht Sinn, ich werde es versuchen, vielen Dank!!!

Ich sollte wahrscheinlich auch hinzufügen, dass Sie Aktionen reiner machen können, indem Sie redux-saga . Redux-saga hat jedoch Schwierigkeiten, mit asynchronen Ereignissen umzugehen, daher können Sie diese Idee noch einen Schritt weiterführen, indem Sie RxJS (oder eine beliebige FRP-Bibliothek) anstelle von redux-saga verwenden. Hier ist ein Beispiel mit KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

Hallo @frankandrobot ,

redux-saga hat Probleme mit asynchronen Ereignissen

Was meinst du damit? Ist redux-saga gemacht, auf elegante Weise mit asynchronen Ereignissen und Nebeneffekten umzugehen? Schau mal unter https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

Nein @IceOnFire . Als ich das letzte Mal die redux-saga Dokumente gelesen habe, ist der Umgang mit komplexen asynchronen Workflows schwierig. Siehe zum Beispiel: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
Es sagte (sagt immer noch?) etwas in der Richtung

die restlichen Details überlassen wir dem Leser, denn es wird langsam komplex

Vergleichen Sie das mit dem FRP-Weg: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
Der gesamte Workflow wird vollständig abgewickelt. (In nur wenigen Zeilen möchte ich hinzufügen.) Darüber hinaus erhalten Sie immer noch das meiste von der Güte von redux-saga (alles ist eine reine Funktion, meist einfache Unit-Tests).

Als ich das letzte Mal darüber nachdachte, kam ich zu dem Schluss, dass das Problem bei Redux-Saga ist, dass alles synchron aussieht. Es eignet sich hervorragend für einfache Workflows, aber für komplexe asynchrone Workflows ist es einfacher, wenn Sie die Asynchronisierung explizit behandeln ... und darin zeichnet sich FRP aus.

Hallo @frankandrobot ,

Danke für Ihre Erklärung. Ich sehe das von Ihnen erwähnte Zitat nicht, vielleicht hat sich die Bibliothek weiterentwickelt (zum Beispiel sehe ich jetzt einen cancel Effekt, den ich noch nie zuvor gesehen habe).

Wenn sich die beiden Beispiele (Saga und FRP) genau gleich verhalten, sehe ich keinen großen Unterschied: Das eine ist eine Folge von Anweisungen innerhalb von try/catch-Blöcken, während das andere eine Kette von Methoden in Streams ist. Aufgrund meiner mangelnden Erfahrung mit Streams finde ich das Saga-Beispiel sogar besser lesbar und testbar, da Sie jeden einzelnen yield einzeln testen können. Aber ich bin mir ziemlich sicher, dass das mehr an meiner Denkweise liegt als an den Technologien.

Wie auch immer, ich würde gerne die Meinung von @yelouafi dazu wissen.

@bvaughn können Sie auf ein anständiges Beispiel für das Testen von Aktion, Reduzierer und Selektor in demselben Test wie hier beschrieben hinweisen?

Die effizienteste Methode zum Testen von Aktionen, Reduzierern und Selektoren besteht darin, beim Schreiben von Tests dem "Enten"-Ansatz zu folgen. Das bedeutet, dass Sie einen Testsatz schreiben sollten, der einen bestimmten Satz von Aktionen, Reduzierern und Selektoren abdeckt, anstatt drei Testsätze, die sich auf jeden einzelnen konzentrieren. Dies simuliert genauer, was in Ihrer realen Anwendung passiert, und bietet das beste Preis-Leistungs-Verhältnis.

Hallo @ morgs32

Dieses Problem ist ein bisschen alt und ich habe Redux schon eine Weile nicht mehr verwendet. Auf der Redux-Site gibt es einen Abschnitt über das Schreiben von Tests , den Sie sich ansehen sollten.

Im Grunde habe ich nur darauf hingewiesen, dass es effizienter sein kann, Tests für Aktionen und Reduzierer isoliert zu schreiben, sie zusammen zu schreiben, wie folgt:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

Dies war nur meine eigene Beobachtung, nachdem ich Redux einige Monate in einem Projekt verwendet hatte. Es ist keine offizielle Empfehlung. YMMV. 👍

"Machen Sie mehr in Action-Creators und weniger in Reducern"

Was ist, wenn die Anwendung Server&Client ist und der Server Geschäftslogik und Validatoren enthalten muss?
Also sende ich die Aktion wie sie ist, und die meiste Arbeit wird serverseitig durch den Reducer erledigt ...

Erstmal Entschuldigung für mein Englisch. Aber ich habe verschiedene Meinungen.

Meine Wahl ist fat Reducer, thin Action Creators.

Meine Aktionsersteller haben nur __dispatch__-Aktionen (async, sync, seriell-async, parallel-async, parallel-async in for-loop) basierend auf einer __versprechenden Middleware__ .

Meine Reduzierer teilen sich in viele kleine State-Slices auf, um die Geschäftslogik zu handhaben. benutze combineReduers kombiniere sie. reducer ist __reine Funktion__, also leicht wiederzuverwenden. Vielleicht verwende ich eines Tages angleJS, ich denke, ich kann mein reducer in meinem Dienst für die gleiche Geschäftslogik wiederverwenden. Wenn Ihr reducer viele Zeilencodes hat, kann es vielleicht in kleinere Reducer aufgeteilt oder einige Funktionen abstrahiert werden.

Ja, es gibt einige zustandsübergreifende Fälle, bei denen A von B, C .. abhängen und B, C asynchrone Daten sind. Wir müssen B,C , um A zu füllen oder zu initialisieren. Deshalb verwende ich crossSliceReducer .

Über __Machen Sie mehr in Action-Creators und weniger in Reducern__.

  1. Wenn Sie redux-thunk oder usw. verwenden. Ja. Sie können auf den vollständigen Status innerhalb von Aktionserstellern unter getState() zugreifen. Dies ist eine Wahl. Oder Sie können einen __crossSliceReducer__ erstellen, damit Sie auch auf den vollständigen Zustand zugreifen können. Sie können einen Zustandsabschnitt verwenden, um Ihren anderen Zustand zu berechnen.

Über __Gerätetests__

reducer ist __reine Funktion__. So ist es einfach zu testen. Im Moment teste ich nur meine Reduzierstücke, weil es am wichtigsten ist als andere Teile.

Um action creators zu testen? Ich denke, wenn sie "fett" sind, ist es vielleicht nicht einfach, zu testen. Vor allem __Async-Action-Ersteller__.

Ich stimme dir @mrdulin zu , so bin ich jetzt auch gegangen.

@mrdulin Ja. Es sieht so aus, als ob Middleware der richtige Ort ist, um Ihre unreine Logik zu platzieren.
Aber für Business Logic Reducer scheint es nicht der richtige Ort zu sein. Sie erhalten am Ende mehrere "synthetische" Aktionen, die nicht das darstellen, was der Benutzer gefragt hat, sondern das, was Ihre Geschäftslogik erfordert.

Eine viel einfachere Wahl besteht darin, einfach einige reine Funktionen/Klassenmethoden aus der Middleware aufzurufen:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

Dies reduziert die Wahrscheinlichkeit von falsch eingegebenen Variablen, die zu subtilen undefinierten Werten führen, es vereinfacht Änderungen an der Struktur Ihres Geschäfts usw.

Mein Argument gegen die Verwendung von Selektoren überall, selbst für triviale Zustandselemente, die keine Memoisierung oder irgendeine Art von Datentransformation erfordern:

  • Sollten Unit-Tests für Reducer nicht bereits falsch eingegebene Zustandseigenschaften abfangen?
  • Veränderungen in der Storestruktur kommen meiner Erfahrung nach state.stuff.item1 in state.stuff.item2 ändern, durchsuchen Sie den Code und ändern ihn überall - genau wie das Ändern des Namens von allem anderen. Dies ist eine häufige Aufgabe und ein Kinderspiel für Leute, die insbesondere IDEs verwenden.
  • Die Verwendung von Selektoren überall ist eine leere Abstraktion. einfache Staatselemente direkt zuzugreifen, nie verstanden. Sicher, Sie gewinnen Konsistenz, indem Sie diese API für den Zugriff auf den Status haben, aber Sie geben die Einfachheit auf.

Natürlich sind Selektoren notwendig, aber ich würde gerne einige andere Argumente hören, um sie zu einer obligatorischen API zu machen.

Aus praktischer Sicht ist ein guter Grund meiner Erfahrung nach, dass Bibliotheken wie reselect oder redux-saga Selektoren nutzen, um auf Statuselemente zuzugreifen. Das reicht mir, um bei den Selektoren zu bleiben.

Philosophisch gesehen sehe ich Selektoren immer als "Getter-Methoden" der Funktionswelt. Und aus dem gleichen Grund, warum ich niemals auf öffentliche Attribute eines Java-Objekts zugreifen würde, würde ich in einer Redux-App niemals direkt auf Substates zugreifen.

@IceOnFire Es gibt nichts zu nutzen, wenn die Berechnung nicht teuer ist oder keine Datentransformation erforderlich ist.

Getter-Methoden mögen in Java gängige Praxis sein, aber auch der direkte Zugriff auf POJOs in JS.

@timotgl

Warum gibt es eine API zwischen dem Store und anderem Redux-Code?

Selektoren sind die öffentliche Abfrage-(Lese-)API eines Reduzierers, Aktionen sind die öffentliche Befehls-(Schreib-)API eines Reduzierers. Die Struktur von Reducer ist das Implementierungsdetail.

Selektoren und Aktionen werden in der UI-Ebene und in der Saga-Ebene (wenn Sie redux-saga verwenden) verwendet, nicht im Reducer selbst.

@sompylasar Ich bin keine Selektoren verwenden, ich kann einfach etwas direkt aus dem Zustand auswählen, wenn es verfügbar gemacht wird, was beabsichtigt ist.

Sie beschreiben eine Möglichkeit, Selektoren als "Lese-API" eines Reduzierers zu betrachten, aber meine Frage war, was es rechtfertigt, Selektoren überhaupt zu einer obligatorischen API zu machen (obligatorisch wie bei der Durchsetzung als Best Practice in einem Projekt, nicht durch eine Bibliothek? Entwurf).

@timotgl Ja, Selektoren sind nicht obligatorisch. Sie sind jedoch eine bewährte Methode, um sich auf zukünftige Änderungen im App-Code vorzubereiten, sodass sie separat umgestaltet werden können:

  • wie ein bestimmter Teil des Zustands strukturiert und angeschrieben ist (das Reduzierer-Bedenken, ein Ort)
  • wie dieser Teil des Zustands verwendet wird (die Benutzeroberfläche und Nebenwirkungen betreffen viele Stellen, von denen der gleiche Teil des Zustands abgefragt wird)

Wenn Sie die Speicherstruktur ändern möchten, müssen Sie ohne Selektoren alle Stellen finden und umgestalten, an denen auf die betroffenen Zustandselemente zugegriffen wird. ersetzen, insbesondere wenn Sie die Zustandsfragmente, die Sie direkt aus dem Speicher erhalten, nicht über einen Selektor weitergeben.

@sompylasar Danke für deinen Beitrag.

Dies könnte möglicherweise eine nicht-triviale Aufgabe sein

Das ist eine berechtigte Sorge, es scheint mir nur ein teurer Kompromiss zu sein. Ich glaube, ich bin noch nie auf eine Zustandsrefaktorierung gestoßen, die solche Probleme verursacht hat. Ich bin jedoch auf "Selektor-Spaghetti" gestoßen, bei denen verschachtelte Selektoren für jedes triviale Unterelement des Staates für ziemliche Verwirrung sorgten. Diese Gegenmaßnahme selbst muss schließlich auch aufrechterhalten werden. Aber ich verstehe den Grund dafür jetzt besser.

@timotgl Ein einfaches Beispiel, das ich öffentlich teilen kann:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

Der Benutzer des Reduzierers kümmert sich nicht darum, ob state, valueOrError oder etwas anderes gespeichert wird. Offengelegt sind die Zustandszeichenfolge (enum), einige häufig verwendete Überprüfungen dieses Zustands, der Wert und der Fehler.

Ich bin jedoch auf "Selektor-Spaghetti" gestoßen, bei denen verschachtelte Selektoren für jedes triviale Unterelement des Staates für ziemliche Verwirrung sorgten.

Wenn diese Verschachtelung durch das Spiegeln der Reduzierer-Verschachtelung (Zusammensetzung) verursacht wurde, würde ich dies nicht empfehlen, das ist das Implementierungsdetail dieses Reduzierers. Im obigen Beispiel exportieren die Reduzierer, die PromiseReducer für einige Teile ihres Zustands verwenden, ihre eigenen Selektoren, die entsprechend diesen Teilen benannt sind. Auch muss nicht jede Funktion, die wie ein Selektor aussieht, exportiert und Teil der Reducer-API sein.

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

Oh, noch etwas, was ich fast vergessen hätte. Funktionsnamen und Exporte/Importe lassen sich gut und sicher verkleinern. Verschachtelte Objektschlüssel – nicht so sehr, Sie benötigen einen geeigneten Datenfluss-Tracer im Minifier, um den Code nicht zu vermasseln.

@timotgl : Bei vielen unserer

Sie können beispielsweise Aktionen direkt von einer verbundenen Komponente aus absetzen, indem Sie this.props.dispatch({type : "INCREMENT"}) ausführen. Wir raten jedoch davon ab, da es die Komponente zwingt, "zu wissen", dass sie mit Redux kommuniziert. Eine eher React-idiomatische Methode besteht darin, gebundene Aktionsersteller zu übergeben, sodass die Komponente einfach this.props.increment() aufrufen kann, und es spielt keine Rolle, ob diese Funktion ein gebundener Redux-Aktionsersteller ist, ein Callback wird übergeben unten von einem Elternteil oder eine Scheinfunktion in einem Test.

Sie können Aktionstypen auch überall von Hand schreiben, aber wir empfehlen, konstante Variablen zu definieren, damit sie importiert und nachverfolgt werden können und die Wahrscheinlichkeit von Tippfehlern verringert wird.

Ebenso hindert Sie nichts daran, auf state.some.deeply.nested.field in Ihren mapState Funktionen oder -Thunks zuzugreifen. Aber wie bereits in diesem Thread beschrieben, erhöht dies die Wahrscheinlichkeit von Tippfehlern, erschwert das Auffinden von Stellen, an denen ein bestimmter Zustand verwendet wird, erschwert das Refactoring und bedeutet, dass jede teure Transformationslogik wahrscheinlich überholt wird - läuft jedes Mal, auch wenn es nicht nötig ist.

Nein, Sie _müssen_ keine Selektoren verwenden, aber sie sind eine gute Architekturpraxis.

Vielleicht möchten Sie meinen Beitrag Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance durchlesen .

@markerikson Ich argumentiere nicht gegen Selektoren im Allgemeinen oder Selektoren für teure Berechnungen. Und ich hatte nie die Absicht, den Versand selbst an eine Komponente zu übergeben :)

Mein Punkt war, dass ich mit dieser Überzeugung nicht einverstanden bin:

Im Idealfall sollten nur Ihre Reduzierfunktionen und Selektoren die genaue Zustandsstruktur kennen. Wenn Sie also ändern, wo sich ein Zustand befindet, müssen Sie nur diese beiden Teile der Logik aktualisieren.

In Ihrem Beispiel mit state.some.deeply.nested.field kann ich den Wert eines Selektors sehen, um dies zu verkürzen. selectSomeDeeplyNestedField() ist ein Funktionsname gegenüber 5 Eigenschaften, die ich falsch verstehen könnte.

Auf der anderen Seite, wenn Sie diese Richtlinie buchstabengetreu befolgen, tun Sie auch const selectSomeField = state => state.some.field; oder sogar const selectSomething = state => state.something; und irgendwann den Aufwand (Import, Export, Test), dies konsequent zu tun rechtfertigt meiner Meinung nach die (umstrittene) Sicherheit und Reinheit nicht mehr. Es ist gut gemeint, aber ich kann den dogmatischen Geist der Richtlinie nicht loswerden. Ich würde den Entwicklern in meinem Projekt vertrauen, dass sie Selektoren mit Bedacht und gegebenenfalls verwenden - weil meine Erfahrung bisher war, dass sie dies tun.

Unter bestimmten Bedingungen konnte ich jedoch verstehen, warum Sie auf der Seite der Sicherheit und Konventionen irren wollten. Vielen Dank für Ihre Zeit und Ihr Engagement. Ich bin insgesamt sehr froh, dass wir redux haben.

Sicher. FWIW gibt es andere Selektorbibliotheken und auch Wrapper um Reselect . Mit https://github.com/planttheidea/selectorator können Sie beispielsweise Schlüsselpfade mit Punktnotation definieren und die Zwischenselektoren intern für Sie erledigen.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen