it-swarm.com.de

Warum benötigen wir Middleware für den asynchronen Datenfluss in Redux?

Laut der Dokumentation "Ohne Middleware unterstützt der Redux Store nur den synchronen Datenfluss" . Ich verstehe nicht, warum das so ist. Warum kann die Containerkomponente nicht die asynchrone API aufrufen und dann dispatch die Aktionen?

Stellen Sie sich zum Beispiel eine einfache Benutzeroberfläche vor: ein Feld und eine Schaltfläche. Wenn der Benutzer die Taste drückt, wird das Feld mit Daten von einem Remote-Server gefüllt.

A field and a button

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

Wenn die exportierte Komponente gerendert wird, kann ich auf die Schaltfläche klicken und die Eingabe wird korrekt aktualisiert.

Beachten Sie die Funktion update im Aufruf connect. Es löst eine Aktion aus, die der App mitteilt, dass sie aktualisiert wird, und führt dann einen asynchronen Aufruf durch. Nach Beendigung des Aufrufs wird der bereitgestellte Wert als Nutzlast einer anderen Aktion abgesetzt.

Was ist falsch an diesem Ansatz? Warum sollte ich Redux Thunk oder Redux Promise verwenden, wie in der Dokumentation angegeben?

EDIT: Ich habe das Redux-Repo nach Hinweisen durchsucht und festgestellt, dass Action Creators in der Vergangenheit reine Funktionen sein mussten. Zum Beispiel hier ist ein Benutzer, der versucht, eine bessere Erklärung für den asynchronen Datenfluss zu liefern:

Der Action Creator selbst ist immer noch eine reine Funktion, aber die Thunk-Funktion, die er zurückgibt, muss es nicht sein, und er kann unsere asynchronen Aufrufe ausführen

Action-Schöpfer müssen nicht mehr rein sein. Also war Thunk/Promise-Middleware in der Vergangenheit definitiv erforderlich, aber es scheint, dass dies nicht mehr der Fall ist?

556
sbichenko

Was ist falsch an diesem Ansatz? Warum sollte ich Redux Thunk oder Redux Promise verwenden, wie in der Dokumentation angegeben?

An diesem Ansatz ist nichts auszusetzen. In einer großen Anwendung ist dies nur unpraktisch, da unterschiedliche Komponenten die gleichen Aktionen ausführen. Möglicherweise möchten Sie einige Aktionen entprellen oder einen lokalen Status wie die automatische Inkrementierung von IDs in der Nähe von Aktionserstellern usw. beibehalten aus Wartungssicht, um Aktionsersteller in separate Funktionen zu extrahieren.

Sie können meine Antwort auf "So senden Sie eine Redux-Aktion mit einer Zeitüberschreitung" lesen, um eine detailliertere Anleitung zu erhalten.

Middleware wie Redux Thunk oder Redux Promise gibt Ihnen nur "Syntaxzucker" zum Versenden von Thunks oder Versprechungen, aber Sie müssen sie nicht verwenden.

Ohne Middleware könnte Ihr Action Creator also so aussehen

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

Aber mit Thunk Middleware können Sie es so schreiben:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

Es gibt also keinen großen Unterschied. Eine Sache, die mir an letzterem Ansatz gefällt, ist, dass es der Komponente egal ist, ob der Aktionsersteller asynchron ist. Normalerweise wird dispatch aufgerufen, und es kann auch mapDispatchToProps verwendet werden, um einen solchen Aktionsersteller mit einer kurzen Syntax usw. zu verknüpfen. Die Komponenten wissen nicht, wie Aktionsersteller implementiert sind, und Sie können zwischen verschiedenen asynchronen Erstellern wechseln Ansätze (Redux Thunk, Redux Promise, Redux Saga) ohne Änderung der Komponenten. Andererseits wissen Ihre Komponenten bei dem erstgenannten expliziten Ansatz genau , dass ein bestimmter Aufruf asynchron ist, und müssen dispatch durch eine Konvention übergeben werden (z B. als Synchronisationsparameter).

Denken Sie auch darüber nach, wie sich dieser Code ändern wird. Angenommen, wir möchten eine zweite Funktion zum Laden von Daten haben und diese in einem einzigen Aktionsersteller kombinieren.

Beim ersten Ansatz müssen wir uns darüber im Klaren sein, welche Art von Action-Schöpfer wir nennen:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

Mit Redux Thunk können Aktionsersteller dispatch das Ergebnis anderer Aktionsersteller und nicht einmal darüber nachdenken, ob diese synchron oder asynchron sind:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

Mit diesem Ansatz können Sie, wenn Sie später möchten, dass Ihre Aktionsersteller den aktuellen Redux-Status anzeigen, einfach das zweite getState Argument verwenden, das an die Thunks übergeben wird, ohne den aufrufenden Code zu ändern:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

Wenn Sie es ändern müssen, um es synchron zu machen, können Sie dies auch tun, ohne einen Aufrufcode zu ändern:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

Der Vorteil der Verwendung von Middleware wie Redux Thunk oder Redux Promise besteht darin, dass den Komponenten nicht bekannt ist, wie Aktionsersteller implementiert sind, ob sie den Redux-Status berücksichtigen, ob sie synchron oder asynchron sind und ob sie andere Aktionsersteller aufrufen oder nicht . Die Kehrseite ist ein bisschen indirekt, aber wir glauben, dass es sich in realen Anwendungen lohnt.

Schließlich sind Redux Thunk und Freunde nur ein möglicher Ansatz für asynchrone Anforderungen in Redux-Apps. Ein weiterer interessanter Ansatz ist Redux Saga , mit dem Sie lange laufende Daemons („Sagas“) definieren können, die Aktionen ausführen, bevor sie ausgeführt werden, und Anforderungen transformieren oder ausführen, bevor sie Aktionen ausgeben. Dies versetzt die Logik von Action-Machern in Sagen. Vielleicht möchten Sie es ausprobieren und später herausfinden, was am besten zu Ihnen passt.

Ich suchte im Redux-Repository nach Hinweisen und stellte fest, dass Action Creators in der Vergangenheit reine Funktionen sein mussten.

Das ist falsch. Die Docs sagten das, aber die Docs waren falsch.
Action-Schöpfer mussten niemals reine Funktionen sein.
Wir haben die Dokumente korrigiert, um dies widerzuspiegeln.

580
Dan Abramov

Das tust du nicht.

Aber ... du solltest Redux-Saga benutzen :)

Dan Abramovs Antwort ist in Bezug auf _redux-thunk_ richtig, aber ich werde ein bisschen mehr über Redux-Saga sprechen, das ist ziemlich ähnlich, aber mächtiger.

Imperativ VS deklarativ

  • DOM: jQuery ist zwingend erforderlich/React ist deklarativ
  • Monaden : IO ist ein Imperativ/Free ist ein Deklarativ
  • Redux-Effekte : _redux-thunk_ ist zwingend erforderlich/_redux-saga_ ist 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 zu verspotten (oder die gesamte Außenwelt, wenn er mit mehr Dingen interagiert ...).

Wenn Sie Mocks verwenden, führen Sie keine funktionale Programmierung durch.

Aus der Sicht der Nebenwirkungen sind Mocks ein Hinweis darauf, dass Ihr Code unrein ist, und nach Ansicht des funktionalen Programmierers beweisen sie, dass etwas nicht in Ordnung ist. Anstatt eine Bibliothek herunterzuladen, um zu überprüfen, ob der Eisberg intakt ist, sollten wir darum herumfahren. Ein eingefleischter TDD/Java-Typ hat mich einmal gefragt, wie Sie sich über Clojure lustig machen. Die Antwort ist, dass wir das normalerweise nicht tun. Wir sehen es normalerweise als Zeichen, dass wir unseren Code überarbeiten müssen.

Quelle

Die Sagas (wie sie in _redux-saga_ implementiert wurden) sind deklarativ und wie die Komponenten Free monad oder React viel einfacher zu testen, ohne dass sie verspotten.

Siehe auch diesen Artikel :

in der modernen FP sollten wir keine Programme schreiben - wir sollten Beschreibungen von Programmen schreiben, die wir dann nach Belieben prüfen, transformieren und interpretieren können.

(Eigentlich ist Redux-Saga wie ein Hybrid: Der Fluss ist entscheidend, aber die Effekte 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, hauptsächlich, weil wir in Flux den Begriff "Aktion" verwenden, der manchmal beide imperativen Codes darstellen kann (_LOAD_USER_) und Ereignisse (_USER_LOADED_). Ich glaube, dass Sie wie beim Event-Sourcing nur Events versenden sollten.

Sagen in der Praxis anwenden

Stellen Sie sich eine App mit einem Link zu einem Benutzerprofil vor. Der idiomatische Weg, dies mit beiden Middlewares 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:

jedes Mal, wenn ein Benutzername angeklickt wird, wird das Benutzerprofil abgerufen und anschließend ein Ereignis mit dem geladenen Profil ausgelöst.

Wie Sie sehen, bietet _redux-saga_ einige Vorteile.

Durch die Verwendung von takeLatest wird zum Ausdruck gebracht, dass Sie nur daran interessiert sind, die Daten des zuletzt angeklickten Benutzernamens abzurufen. Diese Art von Sachen ist schwer mit Stöcken. Sie hätten takeEvery verwenden können, wenn Sie dieses Verhalten nicht möchten.

Sie halten Action-Macher rein. Beachten Sie, dass es weiterhin nützlich ist, actionCreators (in den Sagen put und den Komponenten dispatch) beizubehalten, da dies Ihnen möglicherweise hilft, in Zukunft eine Aktionsvalidierung (Assertions/Flow/TypeScript) hinzuzufügen.

Ihr Code wird viel testbarer, da die Effekte deklarativ sind

Sie brauchen keine rpc-ähnlichen Aufrufe wie actions.loadUser() mehr auszulösen. Ihre Benutzeroberfläche muss nur das senden, was PASSIERT ist. Wir lösen nur Ereignisse aus (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 nicht mehr die Übersetzungsebene zwischen dem, was geschehen ist, und dem, was als Effekt geschehen soll, enthalten müssen

Stellen Sie sich zum Beispiel eine unendliche Scroll-Ansicht vor. _CONTAINER_SCROLLED_ kann zu _NEXT_PAGE_LOADED_ führen, aber ist es wirklich die Verantwortung des scrollbaren Containers, zu entscheiden, ob wir eine andere Seite laden sollen oder nicht? Dann muss er sich über kompliziertere Dinge im Klaren sein, zum Beispiel, ob die letzte Seite erfolgreich geladen wurde oder ob es bereits eine Seite gibt, die zu laden versucht, oder ob keine Elemente mehr zum Laden übrig sind. Ich denke nicht: Für maximale Wiederverwendbarkeit sollte der scrollbare Container nur beschreiben, dass 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 in Thunks zu orchestrieren, indem Sie Timer usw. starten, haben Sie sowieso das gleiche Problem. Und es gibt einen select -Effekt, mit dem Sie jetzt einen Status aus Ihrem Redux-Speicher abrufen können.

Sagas können auf Zeitreise sein und ermöglichen auch komplexe Flow-Logging- und Entwicklungs-Tools, an denen gerade gearbeitet wird. Hier ist eine einfache asynchrone Flussprotokollierung, die bereits implementiert ist:

saga flow logging

Entkopplung

Sagas ersetzen nicht nur Redux Thunks. Sie kommen aus dem Backend/Distributed Systems/Event-Sourcing.

Es ist ein weit verbreitetes Missverständnis, dass Sagen nur dazu da sind, Ihre Redux-Thunks durch bessere Testbarkeit zu ersetzen. Tatsächlich ist dies nur ein Implementierungsdetail von Redux-Saga. Die Verwendung deklarativer Effekte ist aus Gründen der Testbarkeit besser als die Verwendung von Thunks. Das Saga-Muster kann jedoch zusätzlich zu imperativem oder deklarativem Code implementiert werden.

Erstens ist die Saga eine Software, die es ermöglicht, Transaktionen mit langer Laufzeit (etwaige Konsistenz) und Transaktionen über verschiedene begrenzte Kontexte hinweg zu koordinieren (Fachsprache für domänenbasiertes Design).

Um dies für die Frontend-Welt zu vereinfachen, stellen Sie sich vor, es gibt widget1 und widget2. Wenn auf eine Schaltfläche in Widget1 geklickt wird, sollte dies Auswirkungen auf Widget2 haben. Anstatt die beiden Widgets miteinander zu verbinden (dh Widget1 sendet eine Aktion, die auf Widget2 abzielt), sendet Widget1 nur die Aktion, auf die seine Schaltfläche geklickt wurde. Dann hört die Saga auf diesen Button, klickt und aktualisiert dann Widget2, indem ein neues Ereignis entfernt wird, von dem Widget2 Kenntnis hat.

Dies erhöht die Indirektionsebene, die für einfache Apps nicht erforderlich ist, erleichtert jedoch die Skalierung komplexer Anwendungen. 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 Liste von Aktionen 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 für Ihr Unternehmen auf sinnvolle Weise koordinieren.

Einige nette Artikel zur Strukturierung Ihrer Redux-App, in denen Sie Redux-saga aus Entkopplungsgründen 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 stark an das Benachrichtigungssystem gekoppelt sind, das über eigene Geschäftsregeln verfügt (maximal 3 Benachrichtigungen 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 ziemlich schwer mit Gedanken oder Versprechungen umzusetzen.

notifications

Ich habe hier beschrieben, wie das mit der Saga gemacht werden kann

Warum heißt es eine Saga?

Der Begriff Saga stammt aus der Backend-Welt. Ich habe Yassine (den Autor der Redux-Saga) in einer langen Diskussion an diesen Begriff herangeführt.

Ursprünglich wurde dieser Begriff mit einem paper eingeführt, das Saga-Muster sollte verwendet werden, um eventuelle Konsistenz in verteilten Transaktionen zu gewährleisten, aber seine Verwendung wurde von Back-End-Entwicklern auf eine breitere Definition erweitert, sodass es jetzt möglich ist deckt auch das "Prozessmanagermuster" ab (irgendwie ist das ursprüngliche Sagamuster eine spezialisierte Form des Prozessmanagers).

Heute ist der Begriff "Saga" verwirrend, da er zwei verschiedene Dinge beschreiben kann. Da es in Redux-Saga verwendet wird, beschreibt es keine Möglichkeit, verteilte Transaktionen zu verarbeiten, sondern vielmehr eine Möglichkeit, Aktionen in Ihrer App zu koordinieren. _redux-saga_ hätte auch _redux-process-manager_ heißen können.

Siehe auch:

Alternativen

Wenn Sie die Idee, Generatoren zu verwenden, nicht mögen, aber Sie sich für das Saga-Muster und seine Entkopplungseigenschaften interessieren, können Sie dasselbe auch mit redux-observable erreichen, das den Namen epic verwendet. genau das gleiche Muster zu beschreiben, aber 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 Redux-Saga nützliche Ressourcen

2017 berät

  • Überbeanspruchen Sie Redux-saga nicht nur, um es zu benutzen. Nur testbare API-Aufrufe sind es nicht wert.
  • Entfernen Sie in den meisten einfachen Fällen keine Thunks aus Ihrem Projekt.
  • Zögern Sie nicht, Thunks in yield put(someActionThunk) zu senden, wenn dies sinnvoll ist.

Wenn Sie Angst haben, Redux-saga (oder Redux-observable) zu verwenden, aber nur das Entkopplungsmuster benötigen, aktivieren 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' });
  }
});
_
401

Die kurze Antwort : scheint mir eine völlig vernünftige Herangehensweise an das Asynchronitätsproblem zu sein. Mit ein paar Einschränkungen.

Bei der Arbeit an einem neuen Projekt, das wir gerade in meinem Job angefangen haben, hatte ich eine sehr ähnliche Denkweise. Ich war ein großer Fan von Vanilla Redux 'elegantem System zum Aktualisieren des Speichers und zum erneuten Rendern von Komponenten auf eine Weise, die nicht in die Eingeweide eines React Komponentenbaums passt. Es kam mir komisch vor, sich in diesen eleganten dispatch-Mechanismus einzuklinken, um mit Asynchronität umzugehen.

Am Ende ging ich mit einem wirklich ähnlichen Ansatz an das heran, was Sie in einer Bibliothek haben, die ich aus unserem Projekt herausgezählt habe und die wir react-redux-controller nannten.

Ich habe aus ein paar Gründen den oben beschriebenen Ansatz nicht eingehalten:

  1. So wie Sie es geschrieben haben, haben diese Versandfunktionen keinen Zugriff auf das Geschäft. Sie können das umgehen, indem Sie Ihre UI-Komponenten alle Informationen übergeben lassen, die die Dispatching-Funktion benötigt. Aber ich würde argumentieren, dass dies diese UI-Komponenten unnötigerweise mit der Dispatching-Logik koppelt. Und was noch problematischer ist, es gibt keine offensichtliche Möglichkeit für die Dispatching-Funktion, in asynchronen Fortsetzungen auf den aktualisierten Status zuzugreifen.
  2. Die Dispatching-Funktionen haben über den lexikalischen Bereich Zugriff auf dispatch selbst. Dies schränkt die Optionen für das Umgestalten ein, sobald die Anweisung connect außer Kontrolle gerät - und es sieht mit nur dieser einen Methode update ziemlich unhandlich aus. Sie benötigen also ein System, mit dem Sie diese Dispatcher-Funktionen zusammenstellen können, wenn Sie sie in separate Module aufteilen.

Zusammengenommen müssen Sie ein System aufbauen, damit dispatch und der Speicher zusammen mit den Parametern des Ereignisses in Ihre Dispatching-Funktionen eingefügt werden können. Ich kenne drei sinnvolle Ansätze für diese Abhängigkeitsinjektion:

  • redux-thunk tut dies auf funktionale Weise, indem es sie in Ihre Thunks weiterleitet (indem es sie nach Dome-Definitionen überhaupt nicht genau zu Thunks macht). Ich habe nicht mit den anderen dispatch Middleware-Ansätzen gearbeitet, aber ich gehe davon aus, dass sie im Grunde genommen gleich sind.
  • react-Redux-Controller macht dies mit einer Coroutine. Als Bonus erhalten Sie auch Zugriff auf die "Selektoren", die Funktionen, die Sie möglicherweise als erstes Argument an connect übergeben haben, anstatt direkt mit dem unformatierten, normalisierten Speicher arbeiten zu müssen.
  • Sie können dies auch objektorientiert tun, indem Sie sie über eine Vielzahl möglicher Mechanismen in den Kontext this einfügen.

Update

Mir fällt ein, dass ein Teil dieses Rätsels eine Beschränkung von React-Redux ist. Das erste Argument für connect ruft einen Status-Snapshot ab, wird jedoch nicht gesendet. Das zweite Argument wird gesendet, aber nicht der Status. Keines der Argumente erhält einen Thunk, der den aktuellen Status überschreitet, damit der aktualisierte Status zum Zeitpunkt einer Fortsetzung/eines Rückrufs angezeigt wird.

27
acjay

Abramovs Ziel - und im Idealfall jeder - ist es einfach, Komplexität (und asynchrone Aufrufe) an der Stelle zu kapseln, an der es am geeignetsten ist .

Wo ist der beste Ort, um dies im standardmäßigen Redux-Datenfluss zu tun? Wie wäre es mit:

  • Reduzierungen? Auf keinen Fall. Sie sollten reine Funktionen ohne Nebenwirkungen sein. Die Aktualisierung des Geschäfts ist eine ernste und komplizierte Angelegenheit. Kontaminiere es nicht.
  • Dumb View Components? Auf jeden Fall Nein. Sie haben ein Problem: Präsentation und Benutzerinteraktion und sollten so einfach wie möglich sein.
  • Container Components? Möglich, aber nicht optimal. Es ist sinnvoll, dass der Container ein Ort ist, an dem wir die Komplexität der Ansicht zusammenfassen und mit dem Geschäft interagieren, aber:
    • Container müssen komplexer sein als dumme Komponenten, aber es liegt immer noch in einer Verantwortung: Sie müssen Bindungen zwischen Ansicht und Zustand/Geschäft herstellen. Ihre asynchrone Logik ist ein ganz anderes Anliegen.
    • Indem Sie es in einen Container stellen, sperren Sie Ihre asynchrone Logik für eine einzelne Ansicht/Route in einem einzigen Kontext. Schlechte Idee. Im Idealfall ist alles wiederverwendbar und völlig entkoppelt.
  • S irgendein anderes Servicemodul? Schlechte Idee: Sie müssten den Zugriff auf das Geschäft aktivieren, was ein Alptraum für Wartbarkeit/Testbarkeit ist. Es ist besser, mit Redux zu arbeiten und nur mit den bereitgestellten APIs/Modellen auf den Store zuzugreifen.
  • Aktionen und die Middlewares, die sie interpretieren? Warum nicht ?! Für den Anfang ist es die einzige größere Option, die wir noch haben. :-) Logischerweise ist das Aktionssystem eine entkoppelte Ausführungslogik, die Sie von überall aus verwenden können. Es hat Zugang zum Laden und kann weitere Aktionen auslösen. Es liegt in der Verantwortung, den Kontroll- und Datenfluss innerhalb der Anwendung zu organisieren, und die meisten Async-Funktionen passen genau dazu. [.____]
    • Was ist mit den Action Creators? Warum nicht einfach dort und nicht in den Aktionen selbst und in der Middleware asynchronisieren?
      • Erstens und vor allem haben die Entwickler keinen Zugriff auf den Store, wie es Middleware tut. Das heißt, Sie können keine neuen Kontingentaktionen auslösen, keine Informationen aus dem Geschäft lesen, um Ihre asynchrone Funktion zu erstellen usw.
      • Bewahren Sie die Komplexität an einem Ort auf, an dem die Notwendigkeit komplex ist, und halten Sie alles andere einfach. Die Schöpfer können dann einfache, relativ reine Funktionen sein, die leicht zu testen sind.
16
XML

So beantworten Sie die am Anfang gestellte Frage:

Warum kann die Containerkomponente die asynchrone API nicht aufrufen und dann die Aktionen auslösen?

Beachten Sie, dass diese Dokumente für Redux und nicht für Redux plus React bestimmt sind. Redux-Stores , die mit React-Komponenten verbunden sind , können genau das tun, was Sie sagen, ein Plain Jane Redux-Store ohne Middleware jedoch nicht akzeptiere Argumente für dispatch mit Ausnahme von einfachen alten Objekten.

Auf Middleware könnte man natürlich trotzdem verzichten

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

Aber es ist ein ähnlicher Fall, in dem die Asynchronität um Redux gewickelt wird, anstatt von Redux. Middleware ermöglicht also Asynchronität, indem geändert wird, was direkt an dispatch übergeben werden kann.


Das heißt, der Geist Ihres Vorschlags ist, denke ich, gültig. Es gibt sicherlich andere Möglichkeiten, wie Sie mit Asynchronität in einer Redux + React -Anwendung umgehen können.

Ein Vorteil der Verwendung von Middleware besteht darin, dass Sie weiterhin wie gewohnt Aktionsersteller verwenden können, ohne sich genau darum zu kümmern, wie diese verknüpft sind. Wenn Sie beispielsweise redux-thunk verwenden, sieht der von Ihnen geschriebene Code sehr ähnlich aus

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

was sich nicht allzu sehr vom Original unterscheidet - es ist nur ein bisschen gemischt - und connect weiß nicht, dass updateThing asynchron ist (oder sein muss).

Wenn Sie auch Versprechen , Observablen , Sagen oder verrückter Brauch und hoch deklarativ) unterstützen wollten Aktionsersteller, dann kann Redux dies einfach tun, indem Sie ändern, was Sie an dispatch übergeben (auch bekannt als das, was Sie von Aktionserstellern zurückgeben). Kein Mist mit den React Komponenten (oder connect Aufrufen) notwendig.

11
Michelle Tilley

OK, fangen wir an zu sehen, wie Middleware zuerst funktioniert, das beantwortet die Frage, das ist der Quellcode a Funktion pplyMiddleWare in Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

Schauen Sie sich diesen Teil an und sehen Sie, wie aus unserem Versand eine Funktion wird.

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Beachten Sie, dass jede Middleware die Funktionen dispatch und getState als benannte Argumente erhält.

OK, so stellt sich Redux-Thunk als eine der am häufigsten verwendeten Middlewares für Redux vor:

Mit der Redux Thunk-Middleware können Sie Aktionsersteller schreiben, die eine Funktion anstelle einer Aktion zurückgeben. Der Thunk kann verwendet werden, um den Versand einer Aktion zu verzögern oder nur, wenn eine bestimmte Bedingung erfüllt ist. Die innere Funktion empfängt die Speichermethoden dispatch und getState als Parameter.

Wie Sie sehen, wird statt einer Aktion eine Funktion zurückgegeben, dh Sie können warten und sie jederzeit aufrufen, da es sich um eine Funktion handelt ...

Also, was zum Teufel ist Thunk? So wird es in Wikipedia eingeführt:

In der Computerprogrammierung ist ein Thunk ein Unterprogramm, mit dem eine zusätzliche Berechnung in ein anderes Unterprogramm eingefügt wird. Thunks werden hauptsächlich verwendet, um eine Berechnung zu verzögern, bis sie benötigt wird, oder um Operationen am Anfang oder Ende der anderen Unterroutine einzufügen. Sie haben eine Vielzahl anderer Anwendungen zur Compiler-Code-Generierung und in der modularen Programmierung.

Der Begriff entstand als scherzhafte Ableitung von "denken".

Ein Thunk ist eine Funktion, die einen Ausdruck umschließt, um dessen Auswertung zu verzögern.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

Sehen Sie sich an, wie einfach das Konzept ist und wie Sie damit Ihre asynchronen Aktionen verwalten können ...

Das ist etwas, worauf Sie verzichten können, aber denken Sie daran, dass es beim Programmieren immer bessere, ordentlichere und richtigere Möglichkeiten gibt, Dinge zu tun ...

Apply middleware Redux

6
Alireza

Die Verwendung von Redux-saga ist die beste Middleware in der React-Redux-Implementierung.

Beispiel: store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

Und dann saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { Push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

Und dann action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

Und dann reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

Und dann main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

versuch das .. funktioniert

2
sm chinna

Um die Frage zu beantworten:

Warum kann die Containerkomponente die asynchrone API nicht aufrufen und dann die Aktionen auslösen?

Ich würde aus mindestens zwei Gründen sagen:

Der erste Grund ist die Trennung von Bedenken, es ist nicht die Aufgabe des action creator, das api aufzurufen und Daten zurückzugewinnen, Sie müssen zwei Argumente an Ihr action creator function übergeben, das action type und ein payload.

Der zweite Grund ist, dass der redux store auf ein einfaches Objekt mit dem obligatorischen Aktionstyp und optional einem payload wartet (aber auch hier müssen Sie die Nutzdaten übergeben).

Der Aktionsersteller sollte ein einfaches Objekt wie das folgende sein:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

Und die Aufgabe von Redux-Thunk midleware zu dispache das Ergebnis Ihres api call zu dem entsprechenden action.

0
Mselmi Ali

Es gibt Synchronaktionsersteller und dann gibt es Asynchronaktionsersteller.

Ein Ersteller von Synchronaktionen ist einer, der beim Aufrufen sofort ein Aktionsobjekt mit allen relevanten Daten zurückgibt, die an dieses Objekt angehängt sind, und der bereit ist, von unseren Reduzierern verarbeitet zu werden.

Bei Erstellern von asynchronen Aktionen dauert es einige Zeit, bis eine Aktion ausgeführt werden kann.

Per Definition gilt ein Aktionsersteller, der eine Netzwerkanfrage stellt, immer als asynchroner Aktionsersteller.

Wenn Sie asynchrone Aktionsersteller in einer Redux-Anwendung haben möchten, müssen Sie eine sogenannte Middleware installieren, die es Ihnen ermöglicht, mit diesen asynchronen Aktionserstellern umzugehen.

Sie können dies in der Fehlermeldung überprüfen, die besagt, dass benutzerdefinierte Middleware für asynchrone Aktionen verwendet werden soll.

Was ist eine Middleware und warum benötigen wir sie für den asynchronen Datenfluss in Redux?

Im Kontext von Redux-Middleware wie Redux-Thunk hilft uns eine Middleware beim Umgang mit Erstellern von asynchronen Aktionen, da Redux dies nicht ohne weiteres verarbeiten kann.

Mit einer in den Redux-Zyklus integrierten Middleware rufen wir immer noch Aktionsersteller auf, die eine Aktion zurückgeben, die ausgelöst wird. Wenn wir jedoch eine Aktion auslösen, anstatt sie direkt an alle unsere Reduzierer zu senden, werden wir ausgeführt zu sagen, dass eine Aktion durch alle verschiedenen Middleware in der Anwendung gesendet wird.

Innerhalb einer einzelnen Redux-App können wir so viele oder so wenige Middleware haben, wie wir wollen. In den Projekten, an denen wir arbeiten, werden wir größtenteils eine oder zwei Middleware mit unserem Redux-Store verbinden.

Eine Middleware ist eine einfache JavaScript-Funktion, die bei jeder einzelnen Aktion, die wir auslösen, aufgerufen wird. Innerhalb dieser Funktion hat eine Middleware die Möglichkeit zu verhindern, dass eine Aktion an einen der Reduzierer gesendet wird. Sie kann eine Aktion ändern oder einfach mit einer Aktion herumspielen, wie Sie es möchten. Beispielsweise können wir eine Middleware erstellen, die Konsolenprotokolle erstellt jede Aktion, die Sie nur für Ihr Sehvergnügen auslösen.

Es gibt eine enorme Anzahl von Open-Source-Middleware, die Sie als Abhängigkeiten in Ihr Projekt installieren können.

Sie können nicht nur Open-Source-Middleware verwenden oder diese als Abhängigkeiten installieren. Sie können Ihre eigene Middleware schreiben und in Ihrem Redux-Store verwenden.

Eine der am häufigsten verwendeten Anwendungen von Middleware (und um zu Ihrer Antwort zu gelangen) ist der Umgang mit Erstellern asynchroner Aktionen. Die wahrscheinlich beliebteste Middleware ist Redux-Thunk, und es geht darum, Sie beim Umgang mit Erstellern asynchroner Aktionen zu unterstützen.

Es gibt viele andere Arten von Middleware, die Ihnen auch beim Umgang mit Erstellern asynchroner Aktionen helfen.

0
Daniel