Pourquoi avons-nous besoin d'middleware pour async flow dans Redux?

selon le docs, "sans middleware, Redux store ne supporte que le flux de données synchrones" . Je ne comprends pas pourquoi c'est le cas. Pourquoi le composant container ne peut-il pas appeler L'API async, et ensuite dispatch les actions?

par exemple, imaginez un simple UI: un champ et un bouton. Lorsque l'utilisateur appuie sur le bouton, le champ est rempli de données provenant d'un serveur distant.

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>;
    }
}

lorsque le composant exporté est rendu, je peux cliquer sur le bouton et l'entrée est mise à jour correctement.

notez la fonction update dans l'appel connect . Elle distribue une action qui indique à l'Application qu'il met à jour, puis effectue un appel asynchrone. Une fois l'appel terminé, la valeur fournie est expédiée comme charge utile d'une autre action.

Qu'est-ce qui ne va pas avec ceci approche? Pourquoi voudrais-je utiliser Redux Thunk ou Redux Promise, comme le suggère la documentation?

EDIT: j'ai cherché dans le Redux repo des indices, et j'ai trouvé que les créateurs D'Action étaient nécessaires pour être des fonctions pures dans le passé. Par exemple, voici un utilisateur essayant de fournir une meilleure explication pour le flux de données async:

le créateur d'action lui-même est encore une fonction pure, mais le thunk la fonction renvoie n'a pas besoin d'être, et il peut faire de nos appels asynchrones

les créateurs D'Action ne sont plus tenus d'être purs. Donc, thunk/promesse middleware était certainement nécessaire dans le passé, mais il semble que ce n'est plus le cas?

417
demandé sur Qwerty 2016-01-03 00:09:48

7 réponses

Qu'est-ce qui ne va pas avec cette approche? Pourquoi voudrais-je utiliser Redux Thunk ou Redux Promise, comme le suggère la documentation?

il n'y a rien de mal à cette approche. C'est juste un inconvénient dans une application de grande taille parce que vous aurez des composants différents exécutant les mêmes actions, vous pourriez vouloir débounce certaines actions, ou garder un état local comme auto-incrementing IDs près des créateurs d'action, etc. C'est juste plus facile du point de vue de la maintenance, extraire les créateurs d'actions en fonctions séparées.

, Vous pouvez lire ma réponse à "Comment faire pour envoyer un Redux de l'action avec un délai d'attente" pour une présentation détaillée.

Middleware comme Redux Thunk ou la promesse Redux vous donne juste "le sucre de syntaxe" pour l'expédition des malles ou des promesses, mais vous ne pas besoin de l'utiliser.

donc, sans aucun middleware, votre créateur d'action pourrait ressembler à

// 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
}

mais avec Thunk Middleware vous pouvez l'écrire comme ceci:

// 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
}

il n'y a donc pas de grande différence. Une chose que j'aime dans cette dernière approche, c'est que le composant ne se soucie pas que le créateur d'action soit async. Il appelle simplement dispatch normalement, il peut aussi utiliser mapDispatchToProps pour lier un créateur d'action avec une syntaxe courte, etc. Le les composants ne savent pas comment les créateurs d'action sont mis en œuvre, et vous pouvez basculer entre différentes approches asynchrones (Redux Thunk, Redux Promise, Redux Saga) sans changer les composants. D'un autre côté, avec la première approche explicite, vos composants savent exactement qu'un appel spécifique est asynchrone, et nécessite dispatch pour passer par une convention quelconque (par exemple, comme paramètre de synchronisation).

pense aussi à la façon dont ce code va changer. Dire nous voulons avoir une deuxième fonction de chargement de données, et de les combiner dans un créateur d'action unique.

avec la première approche, nous devons être conscients de quel type d'action Créateur nous appelons:

// 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
}

avec Redux Thunk les créateurs d'action peuvent dispatch le résultat d'autres créateurs d'action et ne pensent même pas si ceux-ci sont synchrones ou asynchrones:

// 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!
}

avec cette approche, si vous voulez plus tard vos créateurs d'action pour regarder dans L'état actuel de Redux, vous pouvez juste utiliser le deuxième argument getState passé aux thunks sans modifier le code d'appel du tout:

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 })
      );
  }
}

si vous avez besoin de le changer pour être synchrone, vous pouvez également le faire sans changer n'importe quel code d'appel:

// 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')
  }
}

donc l'avantage d'utiliser des middlewares comme Redux Thunk ou Redux Promise est que les composants ne sont pas conscients de la façon dont les créateurs d'action sont mis en œuvre, et s'ils se soucient de L'état de Redux, s'ils sont synchrones ou asynchrones, et s'ils appellent ou non d'autres créateurs d'action. L'inconvénient est un peu indirect, mais nous pensons que cela en vaut la peine dans les applications réelles.

enfin, Redux Thunk and friends n'est qu'une approche possible des requêtes asynchrones dans les applications Redux. Une autre approche intéressante est Redux Saga qui vous permet de définir des démons de longue durée ("sagas") qui prennent des mesures au fur et à mesure, et transforment ou exécutent des requêtes avant de les exécuter. Cela déplace la logique des créateurs d'action dans les sagas. Vous pourriez vouloir vérifier, et plus tard de choisir ce qui vous convient le plus.

j'ai cherché des indices dans le repo de Redux, et j'ai trouvé que les créateurs D'Action étaient tenus d'être des fonctions pures dans le passé.

c'est incorrect. Les docs ont dit ça, mais les docs étaient mauvais.

Les créateurs d'Action n'ont jamais été tenus d'être des fonctions pures.

Nous avons corrigé les documents pour refléter cela.

449
répondu Dan Abramov 2017-05-23 12:02:59

vous ne le faites pas.

mais... vous devez utiliser redux-saga:)

la réponse de Dan Abramov est juste à propos de redux-thunk mais je vais parler un peu plus sur Redux-saga qui est tout à fait similaire mais plus puissant.

impératif VS déclaratif

  • DOM : jQuery est impératif / Réagir est déclaratif
  • Monades : IO est impératif / Gratuit est déclaratif
  • Redux "effets de 1519380920" : redux-thunk est impératif / redux-saga est déclaratif

quand vous avez un thunk entre vos mains, comme un io monad ou une promesse, vous ne pouvez pas facilement savoir ce qu'il va faire une fois que vous exécutez. La seule façon de tester un thunk est de l'exécuter, et de se moquer du répartiteur (ou du monde entier s'il interagit avec plus truc...).

si vous utilisez des moqueries, alors vous ne faites pas de programmation fonctionnelle.

vus à travers la lentille des effets secondaires, les moqueries sont un drapeau que votre code est impur, et dans l'œil du programmeur fonctionnel, la preuve que quelque chose ne va pas. Au lieu de télécharger une bibliothèque pour nous aider à vérifier que l'iceberg est intact, nous devrions naviguer autour. Un mec hardcore de TDD/Java m'a un jour demandé comment tu te moquais de Clojure. Réponse c'est le cas d'habitude. Nous le voyons généralement comme un signe dont nous avons besoin pour reformuler notre code.

Source

les sagas (comme ils ont été mis en œuvre dans redux-saga ) sont déclaratives et comme les composants libres monad ou React, ils sont beaucoup plus faciles à tester sans aucune moquerie.

Voir Aussi ce article :

dans la FP moderne, nous ne devrait pas écrire des programmes - nous devrions écrire des descriptions de programmes, que nous pouvons ensuite introspecter, transformer, et interpréter à volonté.

(en fait, Redux-saga est comme un hybride: le flux est impératif mais les effets sont déclaratifs)

Confusion: actions/événements / commandes...

Il ya beaucoup de confusion dans le monde de pointe sur la façon dont certains concepts d'arrière-plan comme CQRS / EventSourcing et Flux / Redux peut être lié, principalement parce que dans Flux nous utilisons le terme "action" qui peut parfois représenter à la fois le code impératif ( LOAD_USER ) et les événements ( USER_LOADED ). Je crois que comme l'approvisionnement d'événement, vous devriez seulement expédier des événements.

à l'Aide de sagas dans la pratique

Imaginez une application avec un lien vers un profil d'utilisateur. La façon idiomatique de gérer cela avec les deux middlewares serait:

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 })
  }
}

cette saga se traduit par:"

chaque fois qu'un nom d'utilisateur est cliqué, récupérez le profil de l'utilisateur et ensuite envoyez un événement avec le profil chargé.

comme vous pouvez le voir, il y a certains avantages de redux-saga .

L'utilisation de takeLatest permet d'exprimer qui vous êtes uniquement intéressé à obtenir les données de la dernière nom d'utilisateur cliqué (Gérer les problèmes de concurrence dans le cas où l'utilisateur clique très vite sur un grand nombre de noms d'utilisateur). Ce genre de truc est dur avec les malles. Vous auriez pu utiliser takeEvery si vous ne voulez pas ce comportement.

vous gardez les créateurs d'action pure. Notez qu'il est toujours utile de conserver les créateurs d'action (dans les sagas put et les composants dispatch ), car cela pourrait vous aider à ajouter la validation d'action (assertions/flow/typescript) dans le futur.

votre code devient beaucoup plus testable que les effets sont déclaratifs

vous n'avez plus besoin de déclencher des appels de type rpc comme actions.loadUser() . Votre interface a juste besoin d'envoyer ce qui s'est passé. Nous ne tirons que events (toujours au passé!) et non pas des actions plus. Cela signifie que vous pouvez créer découplé " canards ou contextes délimités et que la saga peut agir comme point de couplage entre ces composants modulaires.

cela signifie que vos vues sont plus faciles à gérer car elles n'ont plus besoin de contenir cette couche de traduction entre ce qui s'est passé et ce qui devrait se passer comme un effet

par exemple, imaginez une vue infinie. CONTAINER_SCROLLED peut conduire à NEXT_PAGE_LOADED , mais est-il vraiment de la responsabilité du conteneur scrollable de décider si oui ou non nous devrions charger une autre page? Ensuite, il doit être conscient de des choses plus compliquées comme si la dernière page a été chargée avec succès ou s'il y a déjà une page qui essaie de charger, ou s'il n'y a plus d'éléments à charger? Je ne le pense pas: pour une réutilisabilité maximale, le conteneur scrollable devrait simplement décrire qu'il a été scrollé. Le chargement d'une page est un "business" effet de défilement

certains pourraient faire valoir que les générateurs peuvent intrinsèquement Cacher l'état en dehors de Redux store avec des variables locales, mais si vous commencez pour orchestrer des choses complexes à l'intérieur des morceaux en démarrant des minuteries etc vous auriez le même problème de toute façon. Et il y a un effet select qui permet maintenant d'obtenir un certain état de votre magasin Redux.

Les Sagas

peuvent être parcourues dans le temps et permettent également la journalisation de flux complexe et les dev-outils qui sont actuellement en cours de développement. Voici un enregistrement simple de flux asynchrone qui est déjà implémenté:

saga flow logging

découplage

Les Sagas

ne remplacent pas seulement les Redux thunks. Ils proviennent de systèmes distribués / backend / event-sourcing.

c'est une idée fausse très répandue que les sagas sont juste là pour remplacer vos Redux thunks avec une meilleure testabilité. En fait c'est juste un détail d'implémentation de redux-saga. L'utilisation d'effets déclaratifs est meilleure que le thunk pour la testabilité, mais le modèle saga peut être mis en œuvre en plus du code impératif ou déclaratif.

en premier lieu, la saga est un logiciel qui permet de coordonner des transactions de longue durée (cohérence éventuelle), et des transactions à travers différents contextes délimités (jargon de conception par domaine).

pour simplifier cela pour le monde frontal, imaginez qu'il y ait widget1 et widget2. Lorsque certaines bouton sur widget1 est cliqué, il devrait avoir un effet sur widget2. Au lieu d'accoupler le 2 les widgets (par ex. widget1 envoi d'une action qui cible widget2), widget1 seulement l'expédition et son bouton a été cliqué. Puis la saga écouter pour ce clic de bouton puis mettre à jour widget2 en dispaching un nouvel événement que widget2 est conscient.

cela ajoute un niveau d'indirecte qui est inutile pour les applications simples, mais le rendent plus facile à l'échelle des applications complexes. Vous pouvez maintenant publier widget1 et widget2 dans différents dépôts npm pour qu'ils n'aient jamais à savoir au sujet de chaque d'autres, sans avoir à partager un registre mondial des actions. Les 2 widgets sont maintenant des contextes limités qui peuvent vivre séparément. Ils n'ont pas besoin les uns des autres pour être compatibles et peuvent être réutilisés dans d'autres applications. La saga est le point de couplage entre les deux widgets qui les coordonnent d'une manière significative pour votre entreprise.

quelques beaux articles sur la façon de structurer votre application Redux, sur laquelle vous pouvez utiliser Redux-saga pour des raisons de découplage:

Un béton de cas d'utilisation: système de notification

je veux que mes composants capable de déclencher l'affichage des notifications in-app. Mais je ne veux pas que mes composants soient fortement couplés au système de notification qui a ses propres règles de fonctionnement (maximum 3 notifications affichées en même temps, mise en file d'attente de notification, durée d'affichage de 4 secondes, etc.)...).

Je ne veux pas que mes composants JSX décident quand une notification apparaîtra/sera cachée. Je lui donne juste la possibilité de demander une notification, et de laisser les règles complexes à l'intérieur de la saga. Ce genre de choses est assez difficile à mettre en œuvre avec des maladresses ou des promesses.

notifications

j'ai décrit ici comment faire avec saga

pourquoi ça s'appelle une Saga?

le terme saga vient du monde de l'arrière-plan. J'ai d'abord présenté Yassine (l'auteur de Redux-saga) à ce terme dans un longue discussion .

initialement, ce terme a été introduit avec un papier , le modèle saga était censé être utilisé pour gérer la cohérence éventuelle dans les transactions distribuées, mais son utilisation a été étendue à une définition plus large par les développeurs d'arrière-plan de sorte qu'il couvre maintenant également le modèle "process manager" (en quelque sorte le modèle saga original est une forme spécialisée de gestionnaire de processus).

Aujourd'hui, le terme "saga" est confus comme il peut décrivez 2 choses différentes. Comme il est utilisé dans redux-saga, il ne décrit pas une façon de gérer les transactions distribuées, mais plutôt une façon de coordonner les actions dans votre application. redux-saga aurait aussi pu être appelé redux-process-manager .

voir aussi:

Alternatives

si vous n'aimez pas l'idée d'utiliser des générateurs mais que vous êtes intéressé par le motif saga et ses propriétés de découplage, vous pouvez également obtenir la même chose avec redux-observable qui utilise le nom epic pour décrire le même schéma, mais avec des RXJ. Si vous connaissez déjà Rx, vous vous sentirez comme chez vous.

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
        }))
    );

Certains redux-saga ressources utiles

2017 advise

  • ne pas abuser Redux-saga juste pour le plaisir de l'utiliser. Les appels API testables ne valent pas la peine.
  • ne retirez pas les malles de votre projet pour les cas les plus simples.
  • n'hésitez pas à envoyer des malles dans yield put(someActionThunk) si cela a du sens.

If vous avez peur D'utiliser Redux-saga (ou Redux-observable) mais vous avez juste besoin du modèle de découplage, cochez redux-dispatch-subscribe : il permet d'écouter les dépêches et de déclencher de nouvelles dépêches dans l'auditeur.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
355
répondu Sebastien Lorber 2017-10-19 15:19:48

La réponse courte : semble totalement approche raisonnable de l'asynchronie de problème pour moi. Avec quelques mises en garde.

j'ai eu une pensée très semblable en travaillant sur un nouveau projet que nous venons de commencer à mon travail. J'ai été un grand fan du système élégant de vanilla Redux pour la mise à jour du magasin et la remise en ordre des composants d'une manière qui reste en dehors des tripes D'un arbre de composants réagir. Ça m'a semblé bizarre de m'accrocher à cette élégante dispatch mécanisme pour gérer l'asynchronie.

j'ai fini par aller avec une approche vraiment similaire à ce que vous avez là dans une bibliothèque que j'ai fait sortir de notre projet, que nous avons appelé react-redux-controller .

j'ai fini par ne pas aller avec l'approche exacte que vous avez ci-dessus pour quelques raisons:

  1. de la façon dont vous l'avez écrit, ces fonctions d'expédition n'ont pas accès au magasin. Vous pouvez en quelque sorte contourner cela en faisant passer vos composants UI dans toutes les informations dont la fonction dispatching a besoin. Mais je dirais que cela jumelle ces composantes D'assurance-chômage à la logique de répartition inutilement. Et plus problématiquement, il n'y a aucun moyen évident pour la fonction dispatching d'accéder à l'état mis à jour dans les continuations asynchrones.
  2. les fonctions de dispatching ont accès à dispatch lui-même via lexical scope. Cela limite les options de reconfiguration une fois que La déclaration connect est hors de contrôle -- et ça a l'air assez compliqué avec juste cette méthode update . Donc vous avez besoin d'un système pour vous permettre de composer ces fonctions de répartiteur si vous les divisez en modules séparés.

dans l'ensemble, vous devez installer un système pour permettre à dispatch et au magasin d'être injecté dans vos fonctions d'expédition, avec les paramètres de l'événement. Je sais de trois approches raisonnables à cette injection de dépendance:

  • redux-thunk fait cela d'une manière fonctionnelle, en les passant dans vos thunks (ce qui les rend pas exactement thunks à tous, selon les définitions dôme). Je n'ai pas travaillé avec les autres approches middleware dispatch , mais je suppose qu'elles sont essentiellement les mêmes.
  • réagissent-redux-contrôleur de fait avec une coroutine. En prime, il vous donne également accès à la "sélecteurs", qui sont les les fonctions que vous avez peut-être passées comme premier argument à connect , plutôt que d'avoir à travailler directement avec le stock brut, normalisé.
  • vous pouvez également le faire de la manière orientée objet en les injectant dans le contexte this , à travers une variété de mécanismes possibles.

mise à Jour

Il me semble qu'une partie de cette énigme est une limitation de réagissent-redux . Le premier argument de connect obtient un état instantané, mais pas d'expédition. Le deuxième argument obtient expédition, mais pas l'état. Aucun des arguments n'obtient un thunk qui se ferme sur l'état actuel, pour pouvoir voir l'état mis à jour au moment d'une continuation/callback.

24
répondu acjay 2016-01-04 13:35:00

le but D'Abramov-et de tout le monde idéalement - est simplement de encapsuler la complexité (et les appels asynchrones) à l'endroit où il est le plus approprié .

Où est le meilleur endroit pour faire ça dans le flux de données standard de Redux? Pourquoi pas:

  • réducteurs ? Aucun moyen. Ils doivent être des fonctions pures sans effets secondaires. La mise à jour du magasin est une affaire sérieuse et compliquée. Ne la contaminez pas.
  • Bête De Voir Les Composants? Certainement Pas. Ils ont une seule préoccupation: la présentation et l'interaction avec les utilisateurs, et doivent être aussi simples que possible.
  • Conteneur De Composants? Possible, mais sous-optimal. Cela a du sens en ce que le conteneur est un endroit où nous encapsulons une certaine complexité liée à la vue et interagissons avec le magasin, mais:
    • les conteneurs doivent être plus complexes que muets composants, mais c'est toujours une seule responsabilité: fournir des fixations entre la vue et l'état/magasin. Votre logique asynchrone est une autre préoccupation.
    • en le plaçant dans un conteneur, vous verrouilleriez votre logique asynchrone dans un contexte unique, pour une vue/route unique. Mauvaise idée. Idéalement, tout est réutilisable et totalement découplé.
  • S ome other Service Module? mauvaise idée: vous auriez besoin d'injecter accès au magasin, qui est un cauchemar en termes de maintenabilité/testabilité. Il est préférable d'utiliser le grain de Redux et d'accéder au magasin uniquement en utilisant les API/modèles fournis.
  • les Actions et les intermédiaires qui les interprètent? pourquoi pas?! Pour commencer, c'est la seule option majeure nous ont quitté. :- ) Plus logiquement, le système d'action est une logique d'exécution découplée que vous pouvez utiliser de n'importe où. Il a accès au magasin et peut envoyer plus d'actions. Il a une seule responsabilité qui est d'organiser le flux de contrôle et de données autour de l'application, et la plupart des async s'inscrit directement dans cela.
    • Qu'en est-il des créateurs D'Action? Pourquoi ne pas simplement faire async là-dedans, au lieu de dans les actions elles-mêmes, et dans Middleware?
      • tout d'abord et surtout, les créateurs n'ont pas accès au magasin, comme le fait middleware. Cela signifie que vous ne pouvez pas envoyer de nouvelles actions contingentes, ne peut pas lire à partir du magasin pour composer votre asynchrone, etc.
      • ainsi, gardez la complexité dans un endroit qui est complexe de nécessité, et garder tout le reste simple. Les créateurs peuvent alors être simples, des fonctions relativement pures qui sont faciles à tester.
13
répondu XML 2018-04-30 13:13:27

pour répondre à la question posée au début:

pourquoi le composant container ne peut-il pas appeler L'API async, puis lancer les actions?

gardez à l'esprit que ces docs sont pour Redux, pas Redux plus React. Redux stores hooked up to React components peut faire exactement ce que vous dites, mais un simple magasin Jane Redux sans middleware n'accepte pas les arguments à dispatch à l'exception de la plaine de vieux objets.

sans middleware, vous pourriez bien sûr encore faire

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

Mais c'est un cas similaire où l'asynchronie est enveloppé autour de Redux plutôt que manipulé par Redux. Ainsi, middleware permet l'asynchronie en modifiant ce qui peut être passé directement à dispatch .


cela dit, l'esprit de votre suggestion est, Je pense que, valide. Il y a certainement d'autres façons de gérer l'asynchronie dans une application Redux + React.

l'un des avantages d'utiliser middleware est que vous pouvez continuer à utiliser les créateurs d'action comme d'habitude sans vous soucier de savoir exactement comment ils sont connectés. Par exemple, en utilisant redux-thunk , le code que vous avez écrit ressemblerait beaucoup à

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);

qui ne semble pas si différent de l'original - il est juste un peu mélangé - et connect ne sait pas que updateThing est (ou doit être) asynchrone.

Si vous aussi vous voulez prendre en charge promesses , observables , sagas , ou fou personnalisé et très déclaratif action créateurs, puis Redux pouvez le faire en changeant juste ce que vous passez à dispatch (aka, ce que vous êtes de retour de l'action des créateurs). Pas d'embrouille avec la réaction composants (ou appels connect ) nécessaires.

9
répondu Michelle Tilley 2016-01-04 06:20:14

OK, commençons à voir comment fonctionne middleware en premier, qui répond tout à fait à la question, c'est le code source applyMiddleWare fonction dans 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
      });
    };
  };
}

Regardez cette partie, voyez comment notre expédition devient une fonction .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • notez que chaque middleware recevra les fonctions dispatch et getState comme arguments nommés.

OK, voici comment Redux-thunk comme l'un des middlewares les plus utilisés pour Redux se présenter:

Redux Thunk middleware vous permet d'écrire les créateurs d'action qui retournent une fonction au lieu d'une action. Le thunk peut être utilisé pour retarder le envoi d'une action, ou à l'envoi que si une certaine condition est rencontrer. La fonction interne reçoit les méthodes de stockage expédition et getState en tant que paramètres.

comme vous le voyez, il retournera une fonction à la place d'une action, signifie que vous pouvez attendre et l'appeler quand vous voulez car c'est une fonction...

alors qu'est-ce que c'est que ce truc? C'est ainsi Qu'il est introduit dans Wikipedia:

dans la programmation informatique, un thunk est un sous-programme utilisé pour injecter calcul supplémentaire dans un autre sous-programme. Les moufettes sont principalement utilisé pour retarder une calcul jusqu'à ce qu'il est nécessaire, ou à insérer les opérations au début ou à la fin de l'autre sous-routine. Ils ont une variété d'autres applications de génération de code de compilateur et dans programmation modulaire.

le terme provient d'un dérivé joculaire de"penser".

un thunk est une fonction qui enveloppe une expression pour retarder son évaluation.

//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;

donc voir comment le concept est facile et comment il peut vous aider à gérer vos actions asynchrones...

C'est quelque chose que vous pouvez vivre sans, mais rappelez-vous que dans la programmation, il y a toujours de meilleures façons de faire les choses...

Apply middleware Redux

3
répondu Alireza 2017-11-19 04:50:15

pour utiliser Redux-saga est le meilleur middleware dans la mise en œuvre de React-redux.

Ex: stocker.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;

et ensuite 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()
       ]
    }

et ensuite action.js

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

et puis réducteur.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; 

et puis 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'));

essayez ceci.. fonctionne

1
répondu sm chinna 2018-02-09 12:03:06