843 votes

Pourquoi avons-nous besoin de middleware pour async flux dans Redux?

Selon les docs, "Sans middleware, Redux magasin prend uniquement en charge synchrone de flux de données". Je ne comprends pas pourquoi c'est le cas. Pourquoi ne peut pas le composant conteneur appel de l'API asynchrone, puis dispatch actions?

Par exemple, imaginez une INTERFACE utilisateur simple: un champ et d'un bouton. Lorsque l'utilisateur appuie sur le bouton, le champ est renseigné avec les données à partir 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>;
    }
}

Lors de l'exportation de composants est rendu, je peux cliquer sur le bouton et les commentaires sont mis à jour correctement.

Remarque l' update fonction dans l' connect appel. 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 distribué dans la charge utile d'une autre action.

Quel est le problème avec cette approche? Pourquoi voudrais-je utiliser Redux Thunk ou Redux de la Promesse, comme le suggère la documentation?

EDIT: j'ai cherché sur le Redux repo pour trouver des indices, et constaté que l'Action Créateurs ont été nécessaires pour être des fonctions pures dans le passé. Pour exemple, voici un utilisateur tente de fournir une meilleure explication pour async flux de données:

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

Action créateurs n'ont plus besoin d'être pur. Donc, thunk/promesse middleware était certainement nécessaire dans le passé, mais il semble que ce n'est plus le cas?

866voto

Dan Points 16670

Quel est le problème avec cette approche? Pourquoi voudrais-je utiliser Redux Thunk ou Redux de la Promesse, comme le suggère la documentation?

Il n'y a rien de mal avec cette approche. C'est juste gênant dans une grande application parce que vous aurez différents composants d'effectuer les mêmes actions, vous pouvez antirebond de certaines actions, ou de garder certains locaux de l'état comme de l'auto-incrémentation Id proches de l'action créateurs, etc. C'est juste plus facile à partir de la maintenance du point de vue de l'extrait d'action des créateurs dans des fonctions distinctes.

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 Redux Promesse vous donne juste de syntaxe "sucre" pour la distribution des thunks ou des promesses, mais vous ne pas avoir à l'utiliser.

Alors, sans aucun middleware, votre action créateur 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
}

Donc, il n'y a pas de différence énorme. Une chose que j'aime à propos de cette dernière approche est que le composant ne se soucie pas que l'action du créateur est asynchrone. Il appelle juste dispatch normalement, il peut également utiliser des mapDispatchToProps de lier cette action créateur avec une syntaxe courte, etc. Les composants ne sais pas comment l'action de créateurs sont mis en œuvre, et vous pouvez basculer entre les différentes async approches (Redux Thunk, Redux Promesse, Redux Saga) sans changer les composants. D'autre part, avec l'ancien, explicite approche, vos composants de savoir exactement qu'un appel spécifique est asynchrone, et les besoins en dispatch à être passés par certaines convention (par exemple, en tant que paramètre sync).

Pensez également à la façon dont ce code va changer. Disons que nous voulons avoir un deuxième chargement des données de la fonction, et de les combiner en un seul geste créateur.

Avec la première approche, nous devons être attentifs à ce genre d'action du 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 d'action des créateurs dispatch le résultat d'une autre action de créateurs et pense même pas que ceux-ci soient 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 plus tard vous voulez que votre action créateurs de regarder dans le courant Redux état, vous pouvez simplement utiliser le deuxième getState argument passé à la thunks sans modifier le code appelant à tous:

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 changer pour être synchrone, vous pouvez également le faire sans modifier le code appelant:

// 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, le bénéfice de l'aide middleware comme Redux Thunk ou Redux Promesse est que les composants ne sont pas conscients de la façon dont l'action créateurs sont mis en œuvre, et qu'ils se soucient de Redux de l'état, qu'ils soient synchrones ou asynchrones, et si oui ou non ils appellent d'autres mesures créateurs. L'inconvénient, c'est un peu d'indirection, mais nous croyons qu'il vaut la peine dans les applications réelles.

Enfin, Redux Thunk et amis est juste une approche possible pour des requêtes asynchrones dans Redux apps. Une autre approche intéressante est Redux Saga qui permet de définir longue démons ("sagas") que de prendre des mesures comme ils viennent, et de transformer ou d'effectuer des demandes avant la sortie d'actions. Cela déplace la logique de l'action et des créateurs dans les sagas. Vous pourriez vouloir vérifier, et plus tard de choisir ce qui vous convient le plus.

J'ai cherché sur le Redux repo pour trouver des indices, et constaté que l'Action Créateurs ont été nécessaires pour être des fonctions pures dans le passé.

Ceci est incorrect. Les docs dit cela, mais les documents étaient des faux.
Action créateurs n'ont jamais été nécessaires pour être des fonctions pures.
Nous avons fixé les docs pour refléter cela.

485voto

Sebastien Lorber Points 9682

Vous n'avez pas.

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

Dan Abramov anwser est juste à propos de la redux-thunk, mais j'en parlerai un peu plus à propos de redux-saga qui est assez semblable, mais plus puissant.

Impératif VS déclarative

  • DOM: JQuery est impératif / Réagir est déclarative
  • Les monades: IO est impératif / Gratuit est déclarative
  • Redux effets: redux-thunk est impératif / redux-saga est déclarative

Lorsque vous avez un thunk dans le vôtre mains, comme un IO monade 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 pour l'exécuter, et se moquent de l'expéditeur (ou l'ensemble du monde extérieur, si elle interagit avec plus de trucs...).

Si vous utilisez des objets fantaisie, alors vous n'êtes pas fait de la programmation fonctionnelle.

Vu à travers le prisme d'effets secondaires, on se moque de sont un indicateur que votre code est impur, et dans le programmeur fonctionnel de l'œil, la preuve que quelque chose est faux. Au lieu de télécharger une bibliothèque pour nous aider à vérifier l'iceberg est intact, nous devrions être en naviguant autour de lui. Un hardcore ATS/Java gars m'a demandé une fois comment vous faites moqueur en Clojure. La réponse est, nous avons l'habitude de ne pas. Nous avons l'habitude de le voir comme un signe, nous avons besoin de revoir notre code.

Source

Les sagas (comme ils l'ont obtenu mis en œuvre dans redux-saga) sont déclaratives et comme la Libre monade ou de Réagir composants, ils sont beaucoup plus faciles à tester sans se moquer.

Voir aussi cet article:

modernes de PF, il ne faut pas écrire des programmes - nous devrions écrire des descriptions de programmes, que nous pouvons ensuite l'introspection, de transformer et de les interpréter.

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

Il y a beaucoup de confusion dans le frontend monde sur la façon dont certains backend concepts comme CQRS / EventSourcing et de Flux / Redux peut être liée, principalement parce que dans le Flux nous utilisons le terme "action", qui peut parfois représenter à la fois du code impératif (LOAD_USER) et des événements (USER_LOADED). Je crois que comme événement-sourcing, vous ne devriez distribuent des événements.

À l'aide de sagas dans la pratique

Imaginez une application où il y a quelque part un lien vers un profil d'utilisateur. Le idiomatiques façon 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* loadUserProfileSaga() {
  while(true) {
    const action = yield take("USER_NAME_CLICKED")
    const userId = action.payload;
    try {
      const userProfile = yield fetch('http://data.com/${userId}')
      yield put({ type: 'USER_PROFILE_LOADED', userProfile })
    } 
    catch(err) {
      yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
    }
  }
}

Cette saga se traduit par:

eveytime un nom d'utilisateur obtient cliqué, de récupérer le profil utilisateur, puis envoi d'un événement avec le profil chargé

Comme vous pouvez le voir, il ya certains avantages de redux-saga.

Vous gardez l'action créateurs pur (en fait, avons-nous encore besoin d'eux?)

Votre code devient beaucoup plus testable que les effets sont déclaratives

Vous pouvez introduire plus complexe de la simultanéité des contrôles à votre saga facilement, comme par exemple si l'utilisateur clique très vite sur user1 et puis, utilisateur2, nous voulons nous assurer que nous serons toujours afficher le profil de l'utilisateur2, peu importe combien de temps chacun des 2 demandes de pris. Edit: ce type de contrôle de concurrence est désormais mis en œuvre dans le haut niveau assistants takeEvery et takeLatest

Vous n'avez pas besoin de plus pour déclencher la rpc comme des appels comme actions.loadUser(). Votre INTERFACE utilisateur a juste besoin de l'expédition ce qui s'EST PASSÉ. Nous n'feu événements (toujours dans le passé!) et pas des actes plus. Cela signifie que vous pouvez créer découplé "canards" ou Délimitée Contextes et que la saga peut agir comme le couplage entre ces composants modulaires.

Cela signifie que votre point de vue est plus facile à gérer car ils n'ont pas besoin de plus pour contenir la couche de traduction entre ce qui a s'est vraiment passé et ce qui doit arriver comme un effet

Par exemple imaginer une infinite de défilement de la vue. CONTAINER_SCROLLED peut entraîner NEXT_PAGE_LOADED, mais est-ce vraiment la responsabilité de l'déroulante contenant de décider oblige à choisir entre lumière ou pas on doit charger une autre page? Ensuite, il doit être au courant de plus de choses compliquées comme les oblige à choisir entre lumière ou pas de la dernière page a été chargé avec succès ou si il existe déjà une page qui essaie de charger, ou si il n'est pas plus à gauche des éléments à charge? Je ne le pense pas: pour un maximum de réutilisabilité le défilement conteneur doit décrire qu'il a fait défiler. Le chargement d'une page est un "business" effet de défilement

Certains pourraient faire valoir que les producteurs peuvent, par nature, état cacher à l'extérieur de redux magasin avec des variables locales, mais si vous commencez à orchestrer des choses complexes à l'intérieur de thunks en commençant timers etc vous avez le même problème de toute façon. Et il y a un select effet qui permet maintenant d'obtenir de l'état de votre Redux magasin.

Sagas peut être le temps parcouru, et permet également des complexes de flux de journalisation et de la dev-tools qui sont actuellement en cours d'élaboration. Voici quelques simples async flux de l'enregistrement qui est déjà mis en œuvre:

saga flow logging

Le découplage

Les Sagas ne sont pas seulement le remplacement de redux thunks. Ils viennent de backend / systèmes distribués / event sourcing.

C'est une idée fausse très répandue que les sagas sont juste là pour remplacer votre redux thunks avec une meilleure testabilité. En fait c'est juste un détail d'implémentation de redux-saga. À l'aide déclarative effets est mieux que les thunks pour la testabilité, mais la saga modèle peut être mis en œuvre sur le dessus de l'impératif ou déclaratif de code.

En premier lieu, la saga est un logiciel qui permet de coordonner long de l'exécution de transactions (cohérence éventuelle), et les transactions sur les différents délimitée contextes (domain driven design jargon).

Pour simplifier ce pour frontend monde, imaginer qu'il y est widget1 et widget2. Lorsque certaines bouton sur widget1 est cliqué, il devrait avoir un effet sur widget2. Au lieu de couplage entre les 2 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 ce bouton, puis cliquez sur mise à jour widget2 par dispaching un nouvel événement qui widget2 est conscient.

Cela ajoute un niveau d'indirection qui est inutile pour des applications simples, mais le rendre plus facile à l'échelle des applications complexes. Vous pouvez maintenant publier widget1 et widget2 à différents mnp dépôts de sorte qu'ils n'ont jamais à se connaître les uns les autres, sans avoir à partager un registre mondial des actions. Les 2 widgets sont maintenant délimité des contextes 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 couplage entre les deux widgets que de les coordonner de manière significative pour votre entreprise.

Quelques bons articles sur la façon de structurer votre Redux application, sur lequel vous pouvez utiliser Redux-saga pour le découplage raisons:

Certains redux-saga ressources utiles

41voto

acjay Points 4797

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

J'ai eu très semblable à la ligne de pensée lorsque l'on travaille sur un nouveau projet, nous avons tout juste de commencer mon travail. J'ai été un grand fan de la vanille Redux élégant système de mise à jour du store et de nouveau rendu composants d'une manière qui reste à l'extérieur de la les entrailles d'un Réagir composant de l'arbre. Il semblait bizarre pour moi de crochet dans cette élégante dispatch mécanisme de gestion de l'asynchronie.

J'ai fini par aller avec un réellement approche similaire à ce que vous avez là dans une bibliothèque, j'ai pris en compte de notre projet, que nous avons appelé réagir-redux-contrôleur.

J'ai fini par ne va pas avec l'approche exacte que vous avez ci-dessus pour plusieurs raisons:

  1. La façon dont vous l'avez écrit, ces dispatching fonctions n'ont pas accès à la boutique. Vous pouvez obtenir un peu autour de vos composants de l'INTERFACE utilisateur passe dans tous les infos pour l'expédition fonction des besoins. Mais je dirais que c'couples de ces composants de l'INTERFACE utilisateur pour l'envoi de la logique inutilement. Et de façon plus problématique, il n'y a aucun moyen évident pour l'expédition de la fonction d'accès de mise à jour de l'état dans async continuations.
  2. La répartition des fonctions ont accès à l' dispatch par le biais de portée lexicale. Cela limite les options pour le refactoring une fois qu' connect déclaration sort de la main -- et il a l'air assez lourd avec juste un update méthode. Si vous avez besoin d'un système pour vous permettre de composer de ceux répartiteur fonctions, si vous les casser en modules distincts.

Prendre ensemble, vous devez monter un système pour permettre l' dispatch et la banque pour être injecté dans votre envoi fonctions, avec les paramètres de l'événement. Je sais de trois approches raisonnables à cette injection de dépendance:

  • redux-thunk ne présente de façon fonctionnelle, en les passant dans votre thunks (pas exactement les thunks à tous, par le dôme de définitions). Je n'ai pas travaillé avec les autres dispatch middleware approches, mais je suppose qu'ils sont fondamentalement les mêmes.
  • réagir-redux-contrôleur de fait avec une coroutine. En prime, il vous donne également accès à la "sélecteurs", qui sont les fonctions dont vous avez peut-être passé comme premier argument à l' connect, plutôt que d'avoir à travailler directement avec les raw, normalisé magasin.
  • Vous pouvez également le faire à la méthode orientée objet en les injectant dans l' this contexte, à travers une variété de mécanismes possibles.

Mise à jour

Il me semble qu'une partie de cette énigme est une limitation de réagir-redux. Le premier argument connect obtient un état instantané, mais pas de distribuer. Le deuxième argument obtient expédition, mais pas l'état. Aucun de ces arguments n'obtient un thunk qui se ferme sur l'état actuel, pour être en mesure de voir la mise à jour de l'état à l'occasion d'une poursuite/de rappel.

19voto

Brandon Tilley Points 49142

Pour répondre à la question qui vous est posée au début:

Pourquoi ne peut pas le composant conteneur appel de l'API asynchrone, puis expédier les actions?

Gardez à l'esprit que ces docs sont pour Redux, pas Redux plus Réagir. Redux magasins accroché à Réagir composants peut faire exactement ce que vous dites, mais une Plaine Jane Redux magasin avec pas de middleware de ne pas accepter des arguments au dispatch à l'exception de la plaine de vieux objets.

Sans middleware vous pouvez bien sûr toujours faire

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

Mais c'est une affaire similaire où l'asynchronie est enroulé autour de Redux plutôt que manipulé par Redux. Donc, middleware permet d'asynchronisme en modifiant ce qui peut être transmis directement à l' dispatch.


Cela dit, l'esprit de votre suggestion, je pense, est valide. Il y a certainement d'autres moyens de gérer l'asynchronie dans un Redux + Réagir application.

Un avantage de l'utilisation du middleware est que vous pouvez continuer à utiliser l'action de créateurs comme d'habitude sans se soucier exactement comment ils sont branchés. Par exemple, à l'aide de redux-thunk, le code que vous avez écrit serait un peu comme

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 n'a pas l'air si différente de l'original - c'est juste mélangé un peu - et connect ne sait pas que l' updateThing est (ou doit être) asynchrone.

Si vous aussi vous voulez prendre en charge des promesses, des observables, sagas, ou fou personnalisé et hautement déclaratif d'action des créateurs, puis Redux pouvez le faire en changeant juste ce que vous lui transmettez dispatch (aka, ce que vous êtes de retour de l'action des créateurs). Pas de coucher avec l'Réagissent composants (ou connect des appels).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X