Das tust du nicht.
Aber ... du solltest Redux-Saga verwenden :)
Dan Abramovs Antwort ist richtig, redux-thunk
aber ich werde ein bisschen mehr über Redux-Saga sprechen , die ziemlich ähnlich, aber mächtiger ist.
Imperativ VS deklarativ
- DOM : jQuery ist zwingend erforderlich / React ist deklarativ
- Monaden : IO ist zwingend / Free ist deklarativ
- Redux-Effekte :
redux-thunk
sind zwingend / redux-saga
deklarativ
Wenn Sie einen Thunk in Ihren Händen haben, wie eine IO-Monade oder ein Versprechen, können Sie nicht leicht wissen, was es tun wird, wenn Sie es ausführen. Die einzige Möglichkeit, einen Thunk zu testen, besteht darin, ihn auszuführen und den Dispatcher (oder die gesamte Außenwelt, wenn er mit mehr Dingen interagiert ...) zu verspotten.
Wenn Sie Mocks verwenden, führen Sie keine funktionale Programmierung durch.
Durch die Linse der Nebenwirkungen gesehen, sind Mocks eine Flagge, dass Ihr Code unrein ist, und im Auge des funktionalen Programmierers ein Beweis dafür, dass etwas nicht stimmt. Anstatt eine Bibliothek herunterzuladen, um zu überprüfen, ob der Eisberg intakt ist, sollten wir um ihn herum segeln. Ein Hardcore-TDD / Java-Typ hat mich einmal gefragt, wie man sich in Clojure lustig macht. Die Antwort lautet: Normalerweise nicht. Wir sehen es normalerweise als Zeichen, dass wir unseren Code umgestalten müssen.
Quelle
Die Sagen (wie sie in implementiert wurden redux-saga
) sind deklarativ und wie die Komponenten Free Monad oder React viel einfacher zu testen, ohne sich zu verspotten.
Siehe auch diesen Artikel :
In modernen FP sollten wir keine Programme schreiben - wir sollten Beschreibungen von Programmen schreiben, die wir dann nach Belieben überprüfen, transformieren und interpretieren können.
(Eigentlich ist die Redux-Saga wie ein Hybrid: Der Fluss ist zwingend, aber die Auswirkungen sind deklarativ.)
Verwirrung: Aktionen / Ereignisse / Befehle ...
In der Frontend-Welt herrscht große Verwirrung darüber, wie einige Backend-Konzepte wie CQRS / EventSourcing und Flux / Redux zusammenhängen können, vor allem, weil wir in Flux den Begriff "Aktion" verwenden, der manchmal sowohl imperativen Code ( LOAD_USER
) als auch Ereignisse () darstellen kann USER_LOADED
). Ich glaube, dass Sie wie beim Event-Sourcing nur Events versenden sollten.
Verwenden von Sagen in der Praxis
Stellen Sie sich eine App mit einem Link zu einem Benutzerprofil vor. Der idiomatische Weg, dies mit jeder Middleware zu handhaben, wäre:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
Diese Saga übersetzt zu:
Rufen Sie jedes Mal, wenn auf einen Benutzernamen geklickt wird, das Benutzerprofil ab und senden Sie dann ein Ereignis mit dem geladenen Profil aus.
Wie Sie sehen können, gibt es einige Vorteile von redux-saga
.
Die Verwendung von takeLatest
Genehmigungen, um auszudrücken, dass Sie nur daran interessiert sind, die Daten des letzten angeklickten Benutzernamens zu erhalten (behandeln Sie Parallelitätsprobleme, falls der Benutzer sehr schnell auf viele Benutzernamen klickt). Diese Art von Sachen ist schwer mit Thunks. Sie hätten verwenden können, takeEvery
wenn Sie dieses Verhalten nicht möchten.
Sie halten Action-Schöpfer rein. Beachten Sie, dass es immer noch nützlich ist, actionCreators (in Sagen put
und Komponenten) beizubehaltendispatch
) , da dies Ihnen möglicherweise helfen wird, die Aktionsvalidierung (Assertions / Flow / Typoskript) in Zukunft hinzuzufügen.
Ihr Code wird viel testbarer, da die Auswirkungen deklarativ sind
Sie müssen keine rpc-ähnlichen Aufrufe mehr auslösen actions.loadUser()
. Ihre Benutzeroberfläche muss nur das versenden, was geschehen ist. Wir feuern nur noch Ereignisse ab (immer in der Vergangenheitsform!) Und keine Aktionen mehr. Dies bedeutet, dass Sie entkoppelte "Enten" oder begrenzte Kontexte erstellen können und dass die Saga als Kopplungspunkt zwischen diesen modularen Komponenten fungieren kann.
Dies bedeutet, dass Ihre Ansichten einfacher zu verwalten sind, da sie diese Übersetzungsebene zwischen dem, was passiert ist und dem, was als Effekt passieren sollte, nicht mehr enthalten müssen
Stellen Sie sich zum Beispiel eine unendliche Bildlaufansicht vor. CONTAINER_SCROLLED
Kann führen zuNEXT_PAGE_LOADED
, aber liegt es wirklich in der Verantwortung des scrollbaren Containers, zu entscheiden, ob wir eine andere Seite laden sollen oder nicht? Dann muss er sich komplizierterer Dinge bewusst sein, z. B. ob die letzte Seite erfolgreich geladen wurde oder ob bereits eine Seite geladen werden soll oder ob keine weiteren Elemente mehr geladen werden müssen. Ich denke nicht: Für maximale Wiederverwendbarkeit sollte der scrollbare Container nur beschreiben, dass er gescrollt wurde. Das Laden einer Seite ist ein "Geschäftseffekt" dieser Schriftrolle
Einige mögen argumentieren, dass Generatoren den Status außerhalb des Redux-Speichers mit lokalen Variablen von Natur aus verbergen können, aber wenn Sie anfangen, komplexe Dinge innerhalb von Thunks durch Starten von Timern usw. zu orchestrieren, hätten Sie sowieso das gleiche Problem. Und es gibt einen select
Effekt, der es jetzt ermöglicht, einen Status aus Ihrem Redux-Shop abzurufen.
Sagas können zeitlich begrenzt sein und ermöglichen auch komplexe Flow-Logging- und Dev-Tools, an denen derzeit gearbeitet wird. Hier ist eine einfache asynchrone Flussprotokollierung, die bereits implementiert ist:
Entkopplung
Sagas ersetzen nicht nur Redux-Thunks. Sie stammen aus dem Backend / verteilten Systemen / Event-Sourcing.
Es ist ein weit verbreitetes Missverständnis, dass Sagen nur dazu da sind, Ihre Redux-Thunks durch bessere Testbarkeit zu ersetzen. Eigentlich ist dies nur ein Implementierungsdetail der Redux-Saga. Die Verwendung deklarativer Effekte ist aus Gründen der Testbarkeit besser als Thunks, aber das Saga-Muster kann zusätzlich zu imperativem oder deklarativem Code implementiert werden.
Erstens ist die Saga eine Software, die es ermöglicht, lang laufende Transaktionen (eventuelle Konsistenz) und Transaktionen über verschiedene begrenzte Kontexte hinweg (domänengesteuerte Designsprache) zu koordinieren.
Stellen Sie sich vor, es gibt Widget1 und Widget2, um dies für die Frontend-Welt zu vereinfachen. Wenn auf eine Schaltfläche in Widget1 geklickt wird, sollte dies Auswirkungen auf Widget2 haben. Anstatt die beiden Widgets miteinander zu koppeln (dh Widget1 löst eine Aktion aus, die auf Widget2 abzielt), sendet Widget1 nur, dass auf die Schaltfläche geklickt wurde. Dann wartet die Saga auf diese Schaltfläche, klickt und aktualisiert dann Widget2, indem ein neues Ereignis entfernt wird, das Widget2 kennt.
Dies fügt eine Indirektionsebene hinzu, die für einfache Apps nicht erforderlich ist, die Skalierung komplexer Anwendungen jedoch einfacher macht. Sie können jetzt Widget1 und Widget2 in verschiedenen npm-Repositorys veröffentlichen, sodass sie sich nie gegenseitig kennen müssen, ohne dass sie eine globale Aktionsregistrierung gemeinsam nutzen müssen. Die 2 Widgets sind jetzt begrenzte Kontexte, die separat leben können. Sie müssen nicht konsistent sein und können auch in anderen Apps wiederverwendet werden. Die Saga ist der Kopplungspunkt zwischen den beiden Widgets, die sie auf sinnvolle Weise für Ihr Unternehmen koordinieren.
Einige nette Artikel zur Strukturierung Ihrer Redux-App, in denen Sie die Redux-Saga aus Gründen der Entkopplung verwenden können:
Ein konkreter Anwendungsfall: Benachrichtigungssystem
Ich möchte, dass meine Komponenten die Anzeige von In-App-Benachrichtigungen auslösen können. Ich möchte jedoch nicht, dass meine Komponenten in hohem Maße an das Benachrichtigungssystem gekoppelt sind, das über eigene Geschäftsregeln verfügt (maximal 3 Benachrichtigungen werden gleichzeitig angezeigt, Benachrichtigungswarteschlange, 4 Sekunden Anzeigezeit usw.).
Ich möchte nicht, dass meine JSX-Komponenten entscheiden, wann eine Benachrichtigung angezeigt / ausgeblendet wird. Ich gebe ihm nur die Möglichkeit, eine Benachrichtigung anzufordern und die komplexen Regeln in der Saga zu belassen. Diese Art von Sachen ist mit Thunks oder Versprechungen ziemlich schwer umzusetzen.
Ich habe hier beschrieben , wie dies mit Saga gemacht werden kann
Warum heißt es Saga?
Der Begriff Saga stammt aus der Backend-Welt. Ich habe Yassine (den Autor der Redux-Saga) in einer langen Diskussion zunächst mit diesem Begriff bekannt gemacht .
Ursprünglich wurde dieser Begriff mit einem Papier eingeführt . Das Saga-Muster sollte verwendet werden, um eventuelle Konsistenz bei verteilten Transaktionen zu behandeln. Die Verwendung wurde jedoch von Backend-Entwicklern auf eine breitere Definition erweitert, sodass es jetzt auch den "Prozessmanager" abdeckt. Muster (irgendwie ist das ursprüngliche Saga-Muster eine spezielle Form des Prozessmanagers).
Heute ist der Begriff "Saga" verwirrend, da er zwei verschiedene Dinge beschreiben kann. Da es in der Redux-Saga verwendet wird, beschreibt es keine Möglichkeit, verteilte Transaktionen zu verarbeiten, sondern eine Möglichkeit, Aktionen in Ihrer App zu koordinieren. redux-saga
hätte auch angerufen werden können redux-process-manager
.
Siehe auch:
Alternativen
Wenn Sie die Idee der Verwendung von Generatoren nicht mögen, sich aber für das Saga-Muster und seine Entkopplungseigenschaften interessieren, können Sie dasselbe auch mit redux-beobachtbar erreichen , das den Namen verwendet epic
, um genau dasselbe Muster zu beschreiben, jedoch mit RxJS. Wenn Sie bereits mit Rx vertraut sind, werden Sie sich wie zu Hause fühlen.
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
Einige nützliche Ressourcen der Redux-Saga
2017 rät
- Überbeanspruchen Sie die Redux-Saga nicht nur, um sie zu verwenden. Nur testbare API-Aufrufe sind es nicht wert.
- Entfernen Sie in den einfachsten Fällen keine Thunks aus Ihrem Projekt.
- Zögern Sie nicht, Thunks einzusenden,
yield put(someActionThunk)
wenn es Sinn macht.
Wenn Sie Angst haben, Redux-Saga (oder Redux-Observable) zu verwenden, aber nur das Entkopplungsmuster benötigen, überprüfen Sie Redux-Dispatch-Subscribe : Es ermöglicht das Abhören von Sendungen und das Auslösen neuer Sendungen im Listener.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});