Pourquoi avons-nous besoin d'un middleware pour le flux asynchrone dans Redux?


687

Selon les documents, "sans middleware, le magasin Redux ne prend en charge que le flux de données synchrone" . Je ne comprends pas pourquoi c'est le cas. Pourquoi le composant conteneur ne peut-il pas appeler l'API asynchrone, puisdispatch les actions?

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

Un champ et un bouton

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 correctement mise à jour.

Notez la updatefonction dans leconnect appel. Il envoie une action qui indique à l'application qu'il est en train de mettre à jour, puis effectue un appel asynchrone. Une fois l'appel terminé, la valeur fournie est envoyée en tant que charge utile d'une autre action.

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

EDIT: J'ai cherché dans le dépôt Redux des indices et j'ai découvert que les créateurs d'actions devaient être de simples fonctions dans le passé. Par exemple, voici un utilisateur essayant de fournir une meilleure explication du flux de données asynchrone:

Le créateur d'action lui-même est toujours une fonction pure, mais la fonction thunk qu'il renvoie n'a pas besoin d'être, et il peut effectuer nos appels asynchrones

Les créateurs d'actions ne doivent plus être purs. Donc, le middleware thunk / promise était définitivement nécessaire dans le passé, mais il semble que ce ne soit plus le cas?


53
Les créateurs d'actions n'ont jamais été tenus d'être de simples fonctions. C'était une erreur dans les documents, pas une décision qui a changé.
Dan Abramov

1
@DanAbramov pour la testabilité, il peut cependant être une bonne pratique. Redux-saga le permet: stackoverflow.com/a/34623840/82609
Sebastien Lorber

Réponses:


702

Quel est le problème 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 gênant dans une grande application, car vous aurez différents composants effectuant les mêmes actions, vous voudrez peut-être rebondir certaines actions, ou garder un état local comme les ID d'incrémentation automatique à proximité des créateurs d'actions, etc. le point de vue de la maintenance pour extraire les créateurs d'actions dans des fonctions distinctes.

Vous pouvez lire ma réponse à «Comment envoyer une action Redux avec un délai d'expiration» pour une procédure plus détaillée.

Un middleware comme Redux Thunk ou Redux Promise ne vous donne que du «sucre de syntaxe» pour l'envoi de thunks ou de promesses, mais vous n'êtes pas obligé de l' utiliser.

Ainsi, sans 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 différence énorme. Une chose que j'aime dans cette dernière approche est que le composant ne se soucie pas que le créateur d'action soit asynchrone. Il appelle simplement dispatchnormalement, il peut également utiliser mapDispatchToPropspour lier un tel créateur d'action avec une courte syntaxe, etc. Les composants ne savent pas comment les créateurs d'action sont implémentés, 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 l'ancienne approche explicite, vos composants savent exactement qu'un appel spécifique est asynchrone et doitdispatch être passé par une convention (par exemple, en tant que paramètre de synchronisation).

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

Avec la première approche, nous devons être conscients du type de créateur d'action que 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'actions peuvent être dispatchle résultat d'autres créateurs d'actions et ne pas même se demander s'ils 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 souhaitez que vos créateurs d'actions examinent plus tard l'état actuel de Redux, vous pouvez simplement utiliser le deuxième getStateargument passé aux thunks sans modifier le code appelant:

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 devez le modifier pour qu'il soit synchrone, vous pouvez également le faire sans modifier aucun 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')
  }
}

Ainsi, l'avantage d'utiliser un middleware comme Redux Thunk ou Redux Promise est que les composants ne savent pas comment les créateurs d'actions sont implémentés, 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'actions. . L'inconvénient est un peu d'indirection, 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 demandes 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 demandes avant de générer des actions. Cela déplace la logique des créateurs d'action vers les sagas. Vous voudrez peut-être le vérifier et choisir plus tard ce qui vous convient le mieux.

J'ai cherché dans le dépôt Redux des indices et j'ai découvert que les créateurs d'action devaient être de simples fonctions dans le passé.

Ceci est une erreur. Les docs l'ont dit, mais les docs étaient faux.
Les créateurs d'actions n'ont jamais été tenus d'être de simples fonctions.
Nous avons corrigé les documents pour refléter cela.


57
Peut-être que la pensée de Dan est courte: le middleware est une approche centralisée, de cette façon vous permet de garder vos composants plus simples et généralisés et de contrôler le flux de données en un seul endroit. Si vous maintenez une grande application, vous l'apprécierez =)
Sergey Lapin

3
@asdfasdfads Je ne vois pas pourquoi cela ne fonctionnerait pas. Cela fonctionnerait exactement de la même manière; mettre alertaprès dispatch()ing l'action.
Dan Abramov

9
Dans votre ligne pénultième premier exemple de code: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Pourquoi dois-je passer en expédition? Si, par convention, il n'y a jamais qu'un seul magasin global, pourquoi ne pas simplement le référencer directement et le faire store.dispatchquand j'en ai besoin, par exemple, dans loadData?
Søren Debois

10
@ SørenDebois Si votre application est uniquement côté client, cela fonctionnera. S'il est rendu sur le serveur, vous voudrez avoir une storeinstance différente pour chaque requête afin que vous ne puissiez pas la définir à l'avance.
Dan Abramov

3
Je veux juste souligner que cette réponse comporte 139 lignes, soit 9,92 fois plus que le code source de redux-thunk qui se compose de 14 lignes: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy

447

Non.

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

La réponse de Dan Abramov est juste, redux-thunkmais je parlerai un peu plus de la redux-saga qui est assez similaire mais plus puissante.

Impératif VS déclaratif

  • DOM : jQuery est impératif / React est déclaratif
  • Monades : IO est impératif / Free est déclaratif
  • Effets redux : redux-thunkest impératif / redux-sagaest déclaratif

Lorsque vous avez un thunk entre vos mains, comme une monade d'E / S ou une promesse, vous ne pouvez pas facilement savoir ce qu'il fera une fois que vous aurez exécuté. La seule façon de tester un thunk est de l'exécuter et de se moquer du répartiteur (ou de tout le monde extérieur s'il interagit avec plus de choses ...).

Si vous utilisez des simulateurs, vous ne faites pas de programmation fonctionnelle.

Vu à travers la lentille des effets secondaires, les simulacres sont un indicateur que votre code est impur, et aux yeux 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 le contourner. Un gars hardcore TDD / Java m'a demandé une fois comment vous vous moquiez de Clojure. La réponse est que ce n'est généralement pas le cas. Nous le voyons généralement comme un signe dont nous avons besoin pour refactoriser notre code.

La source

Les sagas (telles qu'elles ont été implémentées dans redux-saga) sont déclaratives et, comme les composants Free monad ou React, elles sont beaucoup plus faciles à tester sans aucune simulation.

Voir aussi cet article :

dans la PF moderne, nous ne devons pas écrire de programmes - nous devons é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 y a beaucoup de confusion dans le monde du frontend sur la façon dont certains concepts de backend comme CQRS / EventSourcing et Flux / Redux peuvent être liés, principalement parce que dans 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 pour la recherche d'événements, vous ne devez envoyer que des événements.

Utiliser les sagas dans la pratique

Imaginez une application avec un lien vers un profil utilisateur. La façon idiomatique de gérer cela avec chaque middleware 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 utilisateur, puis envoyez un événement avec le profil chargé.

Comme vous pouvez le voir, il y a certains avantages à redux-saga.

L'utilisation de takeLatestpermis pour exprimer que vous êtes uniquement intéressé à obtenir les données du dernier nom d'utilisateur cliqué (gérer les problèmes de concurrence dans le cas où l'utilisateur clique très rapidement sur un grand nombre de noms d'utilisateur). Ce genre de choses est difficile avec les thunks. Vous auriez pu utiliser takeEverysi vous ne vouliez pas ce comportement.

Vous gardez les créateurs d'action purs. Notez qu'il est toujours utile de conserver actionCreators (dans les sagas putet les composants dispatch), car cela pourrait vous aider à ajouter la validation d'action (assertions / flux / typescript) à l'avenir.

Votre code devient beaucoup plus testable car les effets sont déclaratifs

Vous n'avez plus besoin de déclencher des appels de type rpc comme actions.loadUser(). Votre interface utilisateur a juste besoin d'envoyer ce qui s'est passé. Nous ne tirons que des événements (toujours au passé!) Et non plus des actions. Cela signifie que vous pouvez créer des «canards» découplés ou des contextes délimités et que la saga peut servir de 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 produire en tant qu'effet.

Par exemple, imaginez une vue de défilement infinie. CONTAINER_SCROLLEDpeut conduire à NEXT_PAGE_LOADED, mais est-ce vraiment la responsabilité du conteneur déroulant de décider si nous devons ou non charger une autre page? Ensuite, il doit être conscient de 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 tente de se charger, ou s'il n'y a plus d'éléments à charger? Je ne pense pas: pour une réutilisation maximale, le conteneur déroulant devrait simplement décrire qu'il a été fait défiler. Le chargement d'une page est un "effet métier" de ce parchemin

Certains pourraient faire valoir que les générateurs peuvent intrinsèquement masquer l'état en dehors du magasin redux avec des variables locales, mais si vous commencez à orchestrer des choses complexes à l'intérieur des thunks en démarrant des minuteurs, etc., vous auriez de toute façon le même problème. Et il y a un selecteffet qui permet désormais d'obtenir un état de votre boutique Redux.

Les sagas peuvent être parcourues dans le temps et permettent également la journalisation des flux complexes et les outils de développement qui sont actuellement en cours d'élaboration. Voici une journalisation de flux asynchrone simple qui est déjà implémentée:

enregistrement de flux saga

Découplage

Les sagas ne remplacent pas seulement les thunks redux. Ils proviennent du backend / des systèmes distribués / du sourcing d'événements.

C'est une idée fausse très répandue que les sagas sont juste là pour remplacer vos thunks redux avec une meilleure testabilité. En fait, ce n'est qu'un détail de mise en œuvre de redux-saga. L'utilisation des effets déclaratifs est meilleure que les thunks pour la testabilité, mais le modèle de saga peut être implémenté au-dessus du code impératif ou déclaratif.

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

Pour simplifier cela pour le monde frontal, imaginez qu'il existe widget1 et widget2. Lorsqu'un bouton sur widget1 est cliqué, cela devrait avoir un effet sur widget2. Au lieu de coupler les 2 widgets (c'est-à-dire widget1 envoyer une action qui cible widget2), widget1 envoie seulement que son bouton a été cliqué. Ensuite, la saga écoute ce bouton, puis met à jour widget2 en dissociant un nouvel événement dont widget2 a connaissance.

Cela ajoute un niveau d'indirection inutile pour les applications simples, mais facilite la mise à l'échelle d'applications complexes. Vous pouvez désormais publier widget1 et widget2 dans différents référentiels npm afin qu'ils n'aient jamais à se connaître, sans qu'ils aient à partager un registre global d'actions. Les 2 widgets sont maintenant des contextes bornés qui peuvent vivre séparément. Ils n'ont pas besoin les uns des autres pour être cohérents et peuvent également être réutilisés dans d'autres applications. La saga est le point de couplage entre les deux widgets qui les coordonnent de manière significative pour votre entreprise.

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

Un cas d'utilisation concret: le système de notification

Je veux que mes composants puissent déclencher l'affichage des notifications dans l'application. Mais je ne veux pas que mes composants soient fortement couplés au système de notification qui a ses propres règles métier (max 3 notifications affichées en même temps, file d'attente de notification, 4 secondes d'affichage etc ...).

Je ne veux pas que mes composants JSX décident quand une notification s'affichera / se cachera. 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 thunks ou des promesses.

notifications

J'ai décrit ici comment cela peut être fait avec la saga

Pourquoi est-il appelé une saga?

Le terme saga vient du monde du backend. J'ai d'abord présenté Yassine (l'auteur de Redux-saga) à ce terme dans une longue discussion .

Initialement, ce terme a été introduit avec un document , le modèle de saga était censé être utilisé pour gérer la cohérence éventuelle des transactions distribuées, mais son utilisation a été étendue à une définition plus large par les développeurs backend afin qu'il couvre désormais également le "gestionnaire de processus" modèle (en quelque sorte le modèle de saga original est une forme spécialisée de gestionnaire de processus).

Aujourd'hui, le terme "saga" prête à confusion car il peut décrire 2 choses différentes. Comme il est utilisé dans redux-saga, il ne décrit pas un moyen de gérer les transactions distribuées mais plutôt un moyen de coordonner les actions dans votre application. redux-sagaaurait également pu être appelé redux-process-manager.

Voir également:

Alternatives

Si vous n'aimez pas l'idée d'utiliser des générateurs mais que vous êtes intéressé par le modèle de saga et ses propriétés de découplage, vous pouvez également obtenir la même chose avec redux-observable qui utilise le nom epicpour décrire exactement le même modèle, mais avec RxJS. 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
        }))
    );

Quelques ressources utiles de redux-saga

Conseils 2017

  • Ne pas abuser de Redux-saga juste pour le plaisir de l'utiliser. Seuls les appels API testables n'en valent pas la peine.
  • Ne supprimez pas les thunks de votre projet pour la plupart des cas simples.
  • N'hésitez pas à envoyer des thunks yield put(someActionThunk)si cela a du sens.

Si vous avez peur d'utiliser Redux-saga (ou Redux-observable) mais que vous avez juste besoin du schéma de découplage, vérifiez redux-dispatch-subscribe : il permet d'écouter les dépêches et de déclencher de nouvelles dépêches dans l'écouteur.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

64
Cela s'améliore à chaque fois que je revisite. Pensez à en faire un article de blog :).
RainerAtSpirit

4
Merci pour une bonne rédaction. Cependant, je ne suis pas d'accord sur certains aspects. En quoi LOAD_USER est-il impératif? Pour moi, ce n'est pas seulement déclaratif - cela donne également un excellent code lisible. Comme par exemple. "Lorsque j'appuie sur ce bouton, je veux ADD_ITEM". Je peux regarder le code et comprendre exactement ce qui se passe. Si au lieu de cela on l'appelait quelque chose à l'effet de "BUTTON_CLICK", je devrais chercher cela.
swelet

4
Bonne réponse. Il existe maintenant une autre alternative: github.com/blesh/redux-observable
swennemen

4
@swelet désolé pour réponse tardive. Lorsque vous expédiez ADD_ITEM, c'est impératif car vous envoyez une action qui vise à avoir un effet sur votre boutique: vous attendez de l'action qu'elle fasse quelque chose. Être déclaratif embrasse la philosophie de la recherche d'événements: vous ne répartissez pas les actions pour déclencher des changements sur vos applications, mais vous répartissez les événements passés pour décrire ce qui s'est passé dans votre application. L'envoi d'un événement devrait être suffisant pour considérer que l'état de la demande a changé. Le fait qu'il existe un magasin Redux qui réagit à l'événement est un détail d'implémentation facultatif
Sebastien Lorber

3
Je n'aime pas cette réponse car elle détourne l'attention de la vraie question afin de commercialiser la bibliothèque de quelqu'un. Cette réponse fournit une comparaison des deux bibliothèques, ce qui n'était pas l'intention de la question. La vraie question est de savoir s'il faut utiliser un middleware, ce qui est expliqué par la réponse acceptée.
Abhinav Manchanda

31

La réponse courte : me semble être une approche totalement raisonnable du problème d'asynchronie. Avec quelques mises en garde.

J'avais une pensée très similaire en travaillant sur un nouveau projet que nous venons de commencer à mon travail. J'étais un grand fan de l'élégant système de vanilla Redux pour mettre à jour le magasin et rendre les composants d'une manière qui reste en dehors des tripes d'une arborescence de composants React. Il me semblait étrange de me connecter à cet élégant dispatchmécanisme pour gérer l'asynchronie.

J'ai fini par adopter une approche vraiment similaire à ce que vous avez dans une bibliothèque que j'ai exclue de notre projet, que nous avons appelé react-redux-controller .

J'ai fini par ne pas suivre l'approche exacte que vous avez ci-dessus pour quelques raisons:

  1. De la façon dont vous l'avez écrit, ces fonctions de répartition n'ont pas accès au magasin. Vous pouvez quelque peu contourner cela en faisant passer vos composants d'interface utilisateur dans toutes les informations dont la fonction de répartition a besoin. Mais je dirais que cela couple inutilement ces composants d'interface utilisateur à la logique de répartition. Et plus problématique, il n'y a aucun moyen évident pour la fonction de répartition d'accéder à l'état mis à jour dans les continuations asynchrones.
  2. Les fonctions de répartition ont accès à elles- dispatchmêmes via une portée lexicale. Cela limite les options de refactorisation une fois que cette connectinstruction est incontrôlable - et cela semble assez difficile à gérer avec cette seule updateméthode. Vous avez donc besoin d'un système pour vous permettre de composer ces fonctions de répartiteur si vous les divisez en modules séparés.

Ensemble, vous devez configurer un système pour permettre dispatchet injecter le magasin dans vos fonctions de répartition, ainsi que les paramètres de l'événement. Je connais trois approches raisonnables à cette injection de dépendance:

  • redux-thunk le fait de manière fonctionnelle, en les passant dans vos thunks (ce qui en fait pas exactement des thunks, par des définitions de dôme). Je n'ai pas travaillé avec les autres dispatchapproches middleware, mais je suppose qu'elles sont fondamentalement les mêmes.
  • react-redux-controller le fait avec une coroutine. En prime, il vous donne également accès aux "sélecteurs", qui sont les fonctions auxquelles vous avez peut-être passé le premier argument connect, plutôt que d'avoir à travailler directement avec le magasin brut normalisé.
  • Vous pouvez également le faire de manière orientée objet en les injectant dans le thiscontexte, à travers une variété de mécanismes possibles.

Mise à jour

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


22

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

Quel est le meilleur endroit pour le faire dans le flux de données Redux standard? Que diriez-vous:

  • Réducteurs ? En aucune façon. Ce doivent être des fonctions pures sans effets secondaires. La mise à jour du magasin est une affaire sérieuse et compliquée. Ne le contaminez pas.
  • Composants Dumb View? Certainement non. Ils ont une préoccupation: la présentation et l'interaction avec l'utilisateur, et devraient être aussi simples que possible.
  • Composants de conteneur? Possible, mais sous-optimal. Il est logique que le conteneur soit un endroit où nous encapsulons une certaine complexité liée à la vue et interagissions avec le magasin, mais:
    • Les conteneurs doivent être plus complexes que les composants stupides, mais c'est toujours une seule responsabilité: fournir des liaisons entre la vue et l'état / le magasin. Votre logique asynchrone est une toute autre préoccupation.
    • En le plaçant dans un conteneur, vous verrouilleriez votre logique asynchrone dans un seul contexte, pour une seule vue / route. Mauvaise idée. Idéalement, tout est réutilisable et totalement découplé.
  • Un autre module de service? Mauvaise idée: vous devez injecter l'accès au magasin, ce qui est un cauchemar de maintenabilité / testabilité. Mieux vaut aller avec le grain de Redux et accéder au magasin uniquement en utilisant les API / modèles fournis.
  • Les actions et les middlewares qui les interprètent? Pourquoi pas?! Pour commencer, c'est la seule option majeure qui nous reste. :-) 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 tâches asynchrones s'y inscrivent.
    • Qu'en est-il des créateurs d'action? Pourquoi ne pas simplement asynchroniser là-dedans, plutôt que dans les actions elles-mêmes, et dans Middleware?
      • D'abord et surtout, les créateurs n'ont pas accès au magasin, comme le fait le middleware. Cela signifie que vous ne pouvez pas envoyer de nouvelles actions contingentes, ne pouvez pas lire à partir du magasin pour composer votre async, etc.
      • Donc, gardez la complexité dans un endroit complexe par nécessité, et gardez tout le reste simple. Les créateurs peuvent alors être des fonctions simples, relativement pures, faciles à tester.

Composants de conteneurs - pourquoi pas? En raison du rôle que jouent les composants dans React, un conteneur peut agir en tant que classe de service et il obtient déjà un magasin via DI (accessoires). En le plaçant dans un conteneur, vous verrouilleriez votre logique asynchrone dans un seul contexte, pour une seule vue / route - comment cela? Un composant peut avoir plusieurs instances. Il peut être découplé de la présentation, par exemple avec un accessoire de rendu. Je suppose que la réponse pourrait bénéficier encore plus d'exemples courts qui prouvent le point.
Estus Flask

J'adore cette réponse!
Mauricio Avendaño

13

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

Pourquoi le composant conteneur ne peut-il pas appeler l'API asynchrone, puis répartir les actions?

Gardez à l'esprit que ces documents sont pour Redux, pas Redux plus React. Les magasins Redux connectés aux composants React peuvent faire exactement ce que vous dites, mais un magasin Plain Jane Redux sans middleware n'accepte pas d'arguments pour dispatchexclure les objets simples.

Sans middleware, vous pourriez bien sûr toujours faire

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

Mais c'est un cas similaire où l'asynchronie est enroulée autour de Redux plutôt que gérée par Redux. Ainsi, le middleware permet l'asynchronie en modifiant ce qui peut être transmis directement à dispatch.


Cela dit, l'esprit de votre suggestion est, je pense, valide. Il existe certainement d'autres façons de gérer l'asynchronie dans une application Redux + React.

L'un des avantages de l'utilisation du middleware est que vous pouvez continuer à utiliser les créateurs d'actions comme d'habitude sans vous soucier de la façon exacte dont 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 - c'est juste un peu mélangé - et connectne sait pas que updateThingc'est (ou doit être) asynchrone.

Si vous vouliez également soutenir des promesses , des observables , des sagas ou des créateurs d'action personnalisés et hautement déclaratifs , Redux peut le faire simplement en changeant ce que vous passez dispatch(aka, ce que vous retournez des créateurs d'action). Aucun nettoyage avec les composants React (ou connectappels) n'est nécessaire.


Vous conseillez d'envoyer simplement un autre événement à la fin de l'action. Cela ne fonctionnera pas lorsque vous devez afficher une alerte () après la fin de l'action. Les promesses à l'intérieur des composants React fonctionnent cependant. Je recommande actuellement l'approche Promises.
catamphétamine du

8

OK, commençons par voir comment le middleware fonctionne en premier, qui répond tout à fait à la question, voici le code source d'une fonction pplyMiddleWare 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 envoi devient une fonction .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Notez que chaque middleware recevra les fonctions dispatchet en getStatetant qu'arguments nommés.

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

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

Donc, comme vous le voyez, il retournera une fonction à la place d'une action, ce qui signifie que vous pouvez attendre et l'appeler à tout moment car c'est une fonction ...

Alors qu'est-ce que c'est que le thunk? Voilà comment il est introduit dans Wikipedia:

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

Le terme est originaire 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;

Alors voyez à quel point le concept est simple 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 ...

Appliquer le middleware Redux


1
Première fois sur SO, n'a rien lu. Mais j'ai juste aimé le post en regardant la photo. Incroyable, indice et rappel.
Bhojendra Rauniyar

2

Utiliser Redux-saga est le meilleur middleware dans l'implémentation de React-redux.

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

Et puis 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 puis action.js

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

Et puis 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; 

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


3
C'est sérieux pour quelqu'un qui veut simplement appeler un point de terminaison API pour renvoyer une entité ou une liste d'entités. Vous recommandez, "faites juste ceci ... puis ceci, puis ceci, puis cette autre chose, puis que, puis cette autre substance, puis continuez, puis faites ..". Mais l'homme, c'est FRONTEND, nous avons juste besoin d'appeler le BACKEND pour nous donner des données prêtes à être utilisées sur le frontend. Si c'est la voie à suivre, quelque chose ne va pas, quelque chose ne va vraiment pas et quelqu'un n'applique pas KISS de nos jours
zameb

Salut, Utilisez le bloc try and catch pour les appels API. Une fois que l'API a donné la réponse, appelez les types d'actions Réducteur.
SM Chinna

1
@zameb Vous avez peut-être raison, mais votre plainte concerne donc Redux lui-même, et tout ce qu'il a entendu en essayant de réduire la complexité.
jorisw

1

Il existe des créateurs d'actions synchrones, puis des créateurs d'actions asynchrones.

Un créateur d'action synchrone est celui qui, lorsque nous l'appelons, renvoie immédiatement un objet Action avec toutes les données pertinentes attachées à cet objet et prêt à être traité par nos réducteurs.

Les créateurs d'actions asynchrones en sont un dans lequel il faudra un peu de temps avant d'être prêt à envoyer éventuellement une action.

Par définition, chaque fois que vous avez un créateur d'action qui fait une demande réseau, il va toujours être qualifié de créateur d'action asynchrone.

Si vous souhaitez avoir des créateurs d'actions asynchrones dans une application Redux, vous devez installer quelque chose appelé middleware qui vous permettra de traiter avec ces créateurs d'actions asynchrones.

Vous pouvez le vérifier dans le message d'erreur qui nous indique d'utiliser un middleware personnalisé pour les actions asynchrones.

Qu'est-ce qu'un middleware et pourquoi en avons-nous besoin pour le flux asynchrone dans Redux?

Dans le contexte d'un middleware redux tel que redux-thunk, un middleware nous aide à traiter avec les créateurs d'actions asynchrones car c'est quelque chose que Redux ne peut pas gérer hors de la boîte.

Avec un middleware intégré dans le cycle Redux, nous appelons toujours des créateurs d'actions, qui vont retourner une action qui sera envoyée mais maintenant quand nous expédions une action, plutôt que de l'envoyer directement à tous nos réducteurs, nous allons pour dire qu'une action sera envoyée à travers les différents middlewares de l'application.

À l'intérieur d'une seule application Redux, nous pouvons avoir autant ou aussi peu de middleware que nous voulons. Pour la plupart, dans les projets sur lesquels nous travaillons, nous aurons un ou deux middleware connectés à notre magasin Redux.

Un middleware est une simple fonction JavaScript qui sera appelée à chaque action que nous distribuons. À l'intérieur de cette fonction, un middleware a la possibilité d'empêcher une action d'être envoyée à l'un des réducteurs, il peut modifier une action ou simplement jouer avec une action de n'importe quelle manière, par exemple, nous pourrions créer un middleware qui consignera les journaux chaque action que vous envoyez juste pour votre plaisir visuel.

Il existe un grand nombre de middleware open source que vous pouvez installer en tant que dépendances dans votre projet.

Vous n'êtes pas limité à utiliser uniquement des middleware open source ou à les installer en tant que dépendances. Vous pouvez écrire votre propre middleware personnalisé et l'utiliser dans votre boutique Redux.

L'une des utilisations les plus populaires du middleware (et obtenir votre réponse) est de traiter avec les créateurs d'actions asynchrones, probablement le middleware le plus populaire est redux-thunk et il s'agit de vous aider à traiter avec les créateurs d'actions asynchrones.

Il existe de nombreux autres types de middleware qui vous aident également à gérer les créateurs d'actions asynchrones.


1

Pour répondre à la question:

Pourquoi le composant conteneur ne peut-il pas appeler l'API asynchrone, puis répartir les actions?

Je dirais pour au moins deux raisons:

La première raison est la séparation des préoccupations, ce n'est pas le travail de l' action creatorappeler apiet de récupérer les données, vous devez passer deux arguments à votre action creator function, le action typeet unpayload .

La deuxième raison est que le redux storeest en attente d'un objet simple avec le type d'action obligatoire et éventuellement un payload(mais ici, vous devez également transmettre la charge utile).

Le créateur d'action doit être un simple objet comme ci-dessous:

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

Et le travail de Redux-Thunk midlewareà dispachela suite de votre api callau approprié action.


0

Lorsque vous travaillez dans un projet d'entreprise, de nombreuses exigences sont disponibles dans le middleware, telles que (saga) non disponibles dans un flux asynchrone simple, en voici quelques-unes:

  • Exécution de la demande en parallèle
  • Tirer des actions futures sans avoir à attendre
  • Appels non bloquants Effet de course, exemple de prise en charge en premier
  • réponse pour lancer le processus Ordonnancement de vos tâches (premier au premier appel)
  • Composition
  • Annulation de tâche Bifurcation dynamique de la tâche.
  • Prise en charge de l'exécution simultanée de Saga en dehors du middleware redux.
  • Utilisation de canaux

La liste est longue il suffit de revoir la section avancée de la documentation de la saga

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.