web-dev-qa-db-fra.com

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

Selon la documentation, "Sans middleware, Redux Store 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, puis dispatch les actions?

Par exemple, imaginons une interface utilisateur simple: un champ et un bouton. Lorsque l'utilisateur appuie sur le bouton, le champ est rempli avec les données 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. Il distribue une action qui indique à l'application qu'elle se met à 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 recherché des indices dans le référentiel Redux et découvert que les créateurs d'action étaient obligés d'être des fonctions pures dans le passé. Par exemple, voici un utilisateur qui tente de fournir une meilleure explication du flux de données async:

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

Les créateurs d'actions ne sont plus obligés d'être purs. Donc, le middleware thunk/promise était certainement requis dans le passé, mais il semble que ce ne soit plus le cas?

556
sbichenko

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 avec cette approche. C’est tout simplement gênant dans une application volumineuse, car différents composants effectueront les mêmes actions; vous voudrez peut-être éviter certaines actions ou conserver un état local, tel que des identifiants à incrémentation automatique, près des créateurs d’action, etc. le point de vue de la maintenance pour extraire les créateurs d’action dans des fonctions séparées.

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

Un middleware tel que Redux Thunk ou Redux Promise vous donne simplement un "sucre de syntaxe" pour l’envoi de thunks ou de promesses, mais vous n’aurez pas à le faire .

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
}

Donc, il n'y a pas de différence énorme. Une chose que j’aime dans cette dernière approche est que le composant ne se soucie pas du fait que le créateur de l’action est asynchrone. Il appelle simplement dispatch normalement, il peut également utiliser mapDispatchToProps pour lier le créateur de l'action à une syntaxe courte, etc. Les composants ne savent pas comment les créateurs d'action sont implémentés et vous pouvez basculer entre différents programmes asynchrones. approches (Redux Thunk, Redux Promise, Redux Saga) sans changer les composants. De l’autre, avec l’ancienne approche explicite, vos composants savent exactement qu’un appel spécifique est asynchrone et nécessite le passage de dispatch selon une convention (pour exemple, en tant que paramètre de synchronisation).

Pensez également à la façon dont ce code va changer. Supposons que nous voulions une deuxième fonction de chargement de données et que nous les combinions dans un créateur d’action unique.

Avec la première approche, nous devons être attentifs au type d'action créateur 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 dispatch résulter d’autres créateurs d’actions et ne pas même savoir 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’action examinent ultérieurement l’état actuel de Redux, vous pouvez simplement utiliser le second argument getState passé aux thunks sans modifier le code appelant 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 devez le modifier pour qu'il soit synchrone, vous pouvez également le faire sans modifier le 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’utilisation d’un middleware comme Redux Thunk ou Redux Promise réside dans le fait que les composants ne sont pas conscients de la manière dont les créateurs d’action sont implémentés, et qu’ils se soucient de l’état Redux, qu’ils soient synchrones ou asynchrones et qu’ils appellent ou non d’autres créateurs d’action. . L’inconvénient est un peu d’indirection, mais nous pensons que cela en vaut la peine dans de vraies applications.

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 effectuent des actions au fur et à mesure, ainsi que de transformer ou d’exécuter des requêtes avant d’afficher les actions. Cela déplace la logique des créateurs d’action vers les sagas. Vous voudrez peut-être y jeter un coup d'œil et choisir plus tard ce qui vous convient le mieux.

J'ai cherché des indices dans le référentiel Redux et ai découvert que les créateurs d'action étaient tenus d'être des fonctions pures dans le passé.

Ceci est une erreur. Les docs ont dit cela, mais les docs avaient tort.
Les créateurs d’action n’ont jamais été obligés d’être de simples fonctions.
Nous avons corrigé les docs pour refléter cela.

580
Dan Abramov

Vous pas.

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

La réponse de Dan Abramov est exacte à propos de redux-thunk mais je parlerai un peu plus de de la saga redux-saga , qui est assez similaire mais plus puissant.

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-thunk est impératif/redux-saga est déclaratif

Lorsque vous avez un thunk dans vos mains, comme une monade IO ou une promesse, vous ne pouvez pas savoir facilement ce que cela fera 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 de tout le monde extérieur s'il interagit avec plus de choses ...).

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

Vus sous l’angle des effets secondaires, les mocs sont un signe 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 naviguer autour de lui. Un type hardcore TDD/Java m'a un jour demandé comment vous vous moquiez de Clojure. La réponse est, nous ne le faisons généralement pas. Nous y voyons généralement le signe que nous devons refactoriser notre code.

Source

Les sagas (comme elles ont été implémentées dans redux-saga) sont déclaratives et, comme les composants de la monade libre ou React, elles sont beaucoup plus faciles à tester sans aucune maquette.

Voir aussi cet article :

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

(En réalité, 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 frontal sur la manière dont certains concepts backend tels que CQRS/EventSourcing et Flux/Redux peuvent être liés, principalement parce que dans Flux, nous utilisons le terme "action" qui peut parfois représenter les deux codes impératifs (LOAD_USER) et des événements (USER_LOADED). Je pense que, tout comme pour les événements, vous ne devriez envoyer que des événements.

Utiliser des sagas dans la pratique

Imaginez une application avec un lien vers un profil d'utilisateur. La manière 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, puis envoyez un événement avec le profil chargé.

Comme vous pouvez le constater, redux-saga présente certains avantages.

L'utilisation de takeLatest permet de préciser que vous ne souhaitez obtenir que les données du dernier nom d'utilisateur sur lequel vous avez cliqué (gérez les problèmes de simultanéité si l'utilisateur clique très rapidement sur un grand nombre de noms d'utilisateur). Ce genre de choses est difficile avec des thunks. Vous auriez pu utiliser takeEvery si vous ne voulez pas ce comportement.

Vous gardez les créateurs d'action purs. Notez qu'il est toujours utile de conserver actionCreators (dans sagas put et composants dispatch), car cela pourrait vous aider à ajouter une validation d'action (assertions/flow/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 tels que actions.loadUser(). Votre interface utilisateur a juste besoin d'envoyer ce qui est arrivé. Nous ne faisons que déclencher des événements (toujours au passé!) Et non des actions. Cela signifie que vous pouvez créer des "canards" découplés ou des contextes liés et que la saga peut servir de point de couplage entre ces Composants.

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 comme effet.

Par exemple, imaginez une vue de défilement infinie. CONTAINER_SCROLLED peut conduire à NEXT_PAGE_LOADED, mais est-ce vraiment le conteneur qui défile qui a la responsabilité de décider si nous devons ou non charger une autre page? Ensuite, il doit être conscient de choses plus compliquées, comme par exemple 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 ne reste plus d'éléments à charger. Je ne le pense pas: pour une réutilisation maximale, le conteneur défilable doit simplement indiquer qu'il a été défilé. Le chargement d'une page est un "effet commercial" de ce parchemin

Certains pourraient argumenter que les générateurs peuvent intrinsèquement masquer des états en dehors du magasin redux avec des variables locales, mais si vous commencez à orchestrer des choses complexes à l'intérieur de thunks en démarrant des minuteries, etc., vous rencontrerez le même problème. Et il y a un effet select qui permet maintenant d'obtenir un état de votre magasin Redux.

Les Sagas peuvent être parcourues dans le temps et permettent également une journalisation de flux complexe et des outils de développement sur lesquels on travaille actuellement. Voici quelques traces de flux asynchrones déjà implémentées:

saga flow logging

Découplage

Les sagas ne remplacent pas seulement les thinks redux. Ils proviennent de systèmes backend/distribués/d'événements.

C'est une idée fausse très répandue que les sagas ne sont là que pour remplacer vos thunks redux par une meilleure testabilité. En réalité, il ne s'agit que d'un détail de la mise en œuvre de redux-saga. Utiliser des effets déclaratifs est préférable à thunks pour la testabilité, mais le motif saga peut être implémenté 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 dans différents contextes liés (jargon de conception piloté par domaine).

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

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, de sorte qu'ils n'aient jamais à se connaître, sans avoir à partager un registre global d'actions. Les 2 widgets sont maintenant des contextes liés qui peuvent vivre séparément. Ils n'ont pas besoin d'ê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 coordonne de manière significative pour votre entreprise.

Quelques articles de Nice sur la structure de votre application Redux, sur lesquels vous pouvez utiliser Redux-saga pour des raisons de découplage:

Un cas concret: le système de notification

Je souhaite que mes composants puissent 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 commerciales (maximum 3 notifications affichées en même temps, mise en file d'attente des notifications, temps d'affichage de 4 secondes, etc.).

Je ne veux pas que mes composants JSX décident du moment où une notification sera affichée/masquée. Je lui donne simplement 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 saga

Pourquoi s'appelle-t-il une saga?

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

Initialement, ce terme a été introduit avec un papier , 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 backend. les développeurs afin qu’il couvre désormais également le modèle "gestionnaire de processus" (le modèle de saga original est en quelque sorte une forme spécialisée de gestionnaire de processus).

Aujourd'hui, le terme "saga" est déroutant 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 des transactions distribuées mais plutôt un moyen de coordonner des actions dans votre application. redux-saga aurait également pu s'appeler redux-process-manager.

Voir également:

Des alternatives

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

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 sur redux-saga

2017 conseils

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

Si vous avez peur d'utiliser Redux-saga (ou Redux-observable) mais avez simplement besoin du schéma de découplage, cochez redux-dispatch-subscribe : il permet d'écouter les dépêches et d'en déclencher de nouvelles. dépêches dans auditeur.

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

La réponse courte : me semble une approche tout à fait raisonnable du problème d'asynchronisme. Avec quelques mises en garde.

J'avais une pensée très similaire lorsque je travaillais sur un nouveau projet que nous venons de commencer dans mon travail. J'étais un grand fan du système élégant de Vanilla Redux, qui permettait de mettre à jour le magasin et de rendre les composants à nouveau, de manière à rester en dehors de l'arborescence des composants React. Cela me paraissait bizarre d’attacher cet élégant mécanisme dispatch pour gérer l’asynchronisme.

J'ai fini par adopter une approche très similaire à celle que vous avez dans une bibliothèque que j'ai intégrée à notre projet et que nous avons appelée react-redux-controller .

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

  1. Comme vous l'avez écrit, ces fonctions de répartition n'ont pas accès au magasin. Vous pouvez en quelque sorte contourner cela en faisant passer vos composants d'interface utilisateur dans toutes les informations nécessaires à la fonction de répartition. Mais je dirais que cela couple inutilement ces composants d'interface utilisateur à la logique de dispatching. Et plus problématique, il n’existe aucun moyen évident pour la fonction de répartition d’accéder à l’état mis à jour dans les continuations asynchrones.
  2. Les fonctions de dispatching ont accès à dispatch lui-même via la portée lexicale. Cela limite les options de refactorisation une fois que l'instruction connect devient incontrôlable - et que la méthode update n'est pas très maniable. Il vous faut donc un système pour vous permettre de composer ces fonctions de répartiteur si vous les divisez en modules distincts.

Pris ensemble, vous devez configurer un système pour permettre à dispatch et au magasin d’être injectés dans vos fonctions de dispatching, ainsi que les paramètres de l’événement. Je connais trois approches raisonnables pour cette injection de dépendance:

  • redux-thunk le fait de manière fonctionnelle, en les passant dans vos thunks (ce qui ne les rend pas exactement du tout, par définition du dôme). Je n'ai pas travaillé avec les autres approches de middleware dispatch, 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 que vous avez peut-être transmises en tant que premier argument de connect, au lieu de devoir travailler directement avec le magasin brut normalisé.
  • Vous pouvez également le faire de manière orientée objet en les injectant dans le contexte this, à travers une variété de mécanismes possibles.

Mettre à jour

Il me semble qu'une partie de cette énigme est une limitation de react-redux . Le premier argument de connect obtient un instantané d'état, mais pas le dispatch. Le second argument est expédié mais pas l'état. Aucun des arguments ne reçoit un thunk qui se ferme au-dessus de l'état actuel, pour pouvoir voir l'état mis à jour au moment d'une continuation/callback.

27
acjay

Le but d'Abramov - et tout le monde dans l'idéal - consiste simplement à encapsuler la complexité (et les appels asynchrones) à l'endroit le plus approprié .

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

  • Réducteurs? En aucune façon. Ils devraient être des fonctions pures sans effets secondaires. La mise à jour du magasin est une affaire sérieuse et compliquée. Ne le contamine pas.
  • Composants Dumb View? Certainement Non. Ils ont une préoccupation: la présentation et l’interaction avec l’utilisateur, et doivent être aussi simples que possible.
  • Composants du conteneur? Possible, mais sous-optimal. Cela a du sens car 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 les composants idiots, mais cela reste une responsabilité unique: fournir des liaisons entre view et state/store. Votre logique asynchrone est une préoccupation totalement distincte de cela.
    • En le plaçant dans un conteneur, vous verrouilleriez votre logique asynchrone dans un seul contexte, pour une vue/un itinéraire 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 un accès au magasin, ce qui est un cauchemar pour la maintenabilité/la 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 n'importe où. Il a accès au magasin et peut envoyer plus d'actions. Il a la responsabilité unique d’organiser le flux de contrôle et de données autour de l’application, et la plupart des applications asynchrones s’inscrivent parfaitement dans cette configuration.
    • Qu'en est-il des créateurs d'action? Pourquoi ne pas simplement faire asynchrone ici, plutôt que dans les actions elles-mêmes et dans le middleware?
      • Tout d’abord et surtout, les créateurs n’ont pas accès au magasin, contrairement au middleware. Cela signifie que vous ne pouvez pas envoyer de nouvelles actions contingentes, vous ne pouvez pas lire dans le magasin pour composer votre async, etc.
      • Alors, 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 et faciles à tester.
16
XML

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?

N'oubliez pas que ces documents sont destinés à Redux et non à Redux plus React. Les magasins Redux reliés à React composants peuvent faire exactement ce que vous dites, mais un magasin Plain Jane Redux sans middleware ne le fait pas. accepte les arguments de dispatch sauf les objets simples.

Sans middleware, vous pouvez toujours faire

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

Mais c’est un cas similaire où l’asynchronisme est encapsulé autour de Redux plutôt que traité par Redux. Ainsi, le middleware permet l’asynchronisme en modifiant ce qui peut être passé directement à dispatch.


Cela dit, l’esprit de votre proposition est, je pense, valable. Il existe certainement d'autres manières de gérer l'asynchronie dans une application Redux + React.

L'un des avantages de l'utilisation d'un middleware est que vous pouvez continuer à utiliser les créateurs d'action comme d'habitude sans vous soucier de la manière dont ils sont relié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 mélangé un peu - et connect ne sait pas que updateThing est (ou doit être) asynchrone.

Si vous vouliez également soutenir promesses , observables , sagas , ou coutume folle et hautement déclaratif créateurs d’action, alors Redux peut le faire simplement en changeant ce que vous passez à dispatch (c’est-à-dire ce que vous retournez des créateurs d’action). Pas de nettoyage avec les composants React (ou les appels connect) nécessaires.

11
Michelle Tilley

OK, commençons à voir comment le middleware fonctionne en premier, qui répond bien à la question, c'est le code source a pplyMiddleWare 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 dispatch devient une fonction .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Notez que chaque middleware recevra les fonctions dispatch et getState sous forme d'argument nommé.

OK, voici comment Redux-thunk en tant que 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’action qui renvoient une fonction au lieu d’une action. Le thunk peut être utilisé pour retarder l'envoi d'une action ou uniquement si certaines conditions sont remplies. La fonction interne reçoit les méthodes de magasin 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 quand vous le souhaitez car c'est une fonction ...

Alors qu'est-ce que c'est que le thunk? Voilà comment cela 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 a pour origine un dérivé joyeux 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;

Voyez à quel point le concept est simple et comment il peut vous aider à gérer vos actions asynchrones ...

On peut s'en passer, mais souvenez-vous qu'en programmation, il y a toujours de meilleures façons de faire les choses ...

Apply middleware Redux

6
Alireza

Utiliser Redux-saga est le meilleur middleware pour 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 ensuite 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 .. travaille

2
sm chinna

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 du action creator d’appeler le api et de récupérer les données, vous devez passer deux arguments à votre action creator function, le action type et une payload.

La deuxième raison est que le redux store attend un objet brut avec le type d'action obligatoire et éventuellement un payload (mais vous devez également transmettre la charge utile).

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

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

Et le travail de Redux-Thunk midleware à dispache le résultat de votre api call à la action appropriée.

0
Mselmi Ali

Il existe des créateurs d'actions synchrones, puis des créateurs d'action 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 qu'il est prêt à être traité par nos réducteurs.

Les créateurs d’action asynchrone en sont un dans lequel il faudra un peu de temps avant qu’il soit prêt à envoyer une action.

Par définition, chaque fois que vous avez un créateur d’action qui émet une requête réseau, il sera toujours qualifié de créateur d’action asynchrone.

Si vous voulez avoir des créateurs d'action asynchrone dans une application Redux, vous devez installer un logiciel appelé middleware qui va vous permettre de gérer ces créateurs d'action asynchrone.

Vous pouvez le vérifier dans le message d'erreur nous indiquant que nous utilisons un middleware personnalisé pour les actions asynchrones.

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

Dans le contexte de middleware redux tel que redux-thunk, un middleware nous aide à gérer les créateurs d’actions asynchrones car c’est quelque chose que Redux ne peut gérer immédiatement.

Avec un middleware intégré au cycle Redux, nous appelons toujours les créateurs d’action. Cela va renvoyer une action qui sera expédiée, mais lorsque nous envoyons une action au lieu de l’envoyer directement à tous nos réducteurs, nous allons pour dire qu'une action sera envoyée à travers tous les middleware différents au sein de l'application.

À l'intérieur d'une seule application Redux, nous pouvons avoir autant de middleware que nous le souhaitons. La plupart du temps, dans les projets sur lesquels nous travaillons, nous aurons un ou deux middleware connectés à notre magasin Redux.

Un middleware est une fonction JavaScript simple qui sera appelée à chaque action envoyée. À 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 déranger une action de n'importe quelle manière, par exemple, nous pourrions créer un middleware que la console enregistre. chaque action que vous envoyez juste pour votre plus grand plaisir.

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

Vous n'êtes pas limité à l'utilisation de middleware open source ou à leur installation en tant que dépendances. Vous pouvez écrire votre propre middleware personnalisé et l'utiliser dans votre magasin Redux.

L'une des utilisations les plus courantes du middleware (et pour en arriver à votre réponse) est de traiter avec les créateurs d'actions asynchrones, probablement le middleware le plus populaire actuellement est redux-thunk et vise à 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.

0
Daniel