Comment afficher un indicateur de chargement dans l'application React Redux lors de la récupération des données? [fermé]

107

Je suis nouveau sur React / Redux. J'utilise un middleware fetch api dans l'application Redux pour traiter les API. C'est ( redux-api-middleware ). Je pense que c'est le bon moyen de traiter les actions d'API asynchrones. Mais je trouve des cas qui ne peuvent être résolus par moi-même.

Comme l'indique la page d'accueil ( cycle de vie ), un cycle de vie d'API d'extraction commence par l'envoi d'une action CALL_API se termine par l'envoi d'une action FSA.

Mon premier cas est donc d'afficher / de masquer un préchargeur lors de la récupération des API. L'intergiciel enverra une action FSA au début et une action FSA à la fin. Les deux actions sont reçues par des réducteurs qui ne devraient effectuer qu'un traitement normal des données. Aucune opération d'interface utilisateur, plus d'opérations. Peut-être que je devrais enregistrer l'état du traitement dans l'état puis les rendre lors de la mise à jour du magasin.

Mais comment faire ça? Un flux de composants de réaction sur toute la page? que se passe-t-il avec la mise à jour du magasin à partir d'autres actions? Je veux dire, ce sont plus des événements que des états!

Même dans le pire des cas, que dois-je faire lorsque je dois utiliser la boîte de dialogue de confirmation native ou la boîte de dialogue d'alerte dans les applications redux / react? Où doivent-ils être mis, actions ou réducteurs?

Meilleurs vœux! Souhait de répondre.

企业 应用 架构 模式 大师
la source
1
Annulé la dernière modification de cette question car elle a changé le point entier de la question et des réponses ci-dessous.
Gregg B
Un événement est le changement d'état!
企业 应用 架构 模式 大师
Jetez un œil à questrar. github.com/orar/questrar
Orar

Réponses:

152

Je veux dire, ce sont plus des événements que des états!

Je ne le dirais pas. Je pense que les indicateurs de chargement sont un excellent cas d'interface utilisateur qui se décrit facilement comme une fonction d'état: dans ce cas, d'une variable booléenne. Bien que cette réponse soit correcte, j'aimerais fournir un code pour l'accompagner.

Dans l' asyncexemple du repo Redux , le réducteur met à jour un champ appeléisFetching :

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: true,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: false,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

Le composant utilise connect()de React Redux pour s'abonner à l'état du magasin et retourne isFetchingdans le cadre de la mapStateToProps()valeur de retour afin qu'il soit disponible dans les accessoires du composant connecté:

function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Enfin, le composant utilise isFetchingprop dans la render()fonction pour rendre une étiquette «Loading ...» (qui pourrait éventuellement être un spinner à la place):

{isEmpty
  ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
  : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
      <Posts posts={posts} />
    </div>
}

Même dans le pire des cas, que dois-je faire lorsque je dois utiliser la boîte de dialogue de confirmation native ou la boîte de dialogue d'alerte dans les applications redux / react? Où doivent-ils être mis, actions ou réducteurs?

Les effets secondaires (et l'affichage d'une boîte de dialogue est très certainement un effet secondaire) n'appartiennent pas aux réducteurs. Pensez aux réducteurs comme à des «bâtisseurs d'État» passifs. Ils ne «font» pas vraiment les choses.

Si vous souhaitez afficher une alerte, faites-le depuis un composant avant d'envoyer une action, ou faites-le depuis un créateur d'action. Au moment où une action est envoyée, il est trop tard pour effectuer des effets secondaires en réponse.

Pour chaque règle, il y a une exception. Parfois, votre logique d'effets secondaires est si compliquée que vous voulez en fait les coupler soit à des types d'action spécifiques, soit à des réducteurs spécifiques. Dans ce cas, consultez des projets avancés tels que Redux Saga et Redux Loop . Ne faites cela que lorsque vous êtes à l'aise avec la vanille Redux et que vous avez un réel problème d'effets secondaires dispersés que vous aimeriez rendre plus gérables.

Dan Abramov
la source
16
Et si j'ai plusieurs récupérations en cours? Cependant, une variable ne suffirait pas.
philk
1
@philk si vous avez plusieurs extractions, vous pouvez les regrouper Promise.allen une seule promesse, puis envoyer une seule action pour toutes les extractions. Ou vous devez maintenir plusieurs isFetchingvariables dans votre état.
Sebastien Lorber
2
Veuillez regarder attentivement l'exemple auquel je renvoie. Il y a plus d'un isFetchingdrapeau. Il est défini pour chaque ensemble d'objets en cours de récupération. Vous pouvez utiliser la composition du réducteur pour implémenter cela.
Dan Abramov le
3
Notez que si la demande échoue et RECEIVE_POSTSn'est jamais déclenchée, le signe de chargement restera en place sauf si vous avez créé une sorte de délai d'expiration pour afficher un error loadingmessage.
James111
2
@TomiS - Je liste explicitement toutes mes propriétés isFetching à partir de la persistance redux que j'utilise.
duhseekoh
22

Grande réponse Dan Abramov! Je veux juste ajouter que je faisais plus ou moins exactement cela dans l'une de mes applications (en gardant isFetching comme un booléen) et que j'ai fini par en faire un entier (qui finit par lire comme le nombre de demandes en suspens) pour prendre en charge plusieurs simultanées demandes.

avec booléen:

request 1 démarre -> spinner on -> request 2 start -> request 1 ends -> spinner off -> request 2 ends

avec un entier:

demande 1 démarre -> spinner on -> demande 2 démarre -> demande 1 se termine -> demande 2 se termine -> spinner off

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching + 1,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching - 1,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt
Nuno Campos
la source
2
Ceci est raisonnable. Cependant, le plus souvent, vous souhaitez également stocker certaines données que vous récupérez en plus de l'indicateur. À ce stade, vous devrez avoir plus d'un objet avec isFetchingindicateur. Si vous regardez de près l'exemple que j'ai lié, vous verrez qu'il n'y a pas un objet avec isFetchedmais plusieurs: un par sous-reddit (ce qui est récupéré dans cet exemple).
Dan Abramov le
2
Oh. ouais je n'ai pas remarqué ça. Cependant, dans mon cas, j'ai une entrée globale isFetching dans l'état et une entrée de cache où les données récupérées sont stockées, et pour mes besoins, je me soucie vraiment du fait qu'une activité réseau se produit, peu importe pourquoi
Nuno Campos
4
Oui! Cela dépend si vous souhaitez afficher l'indicateur de récupération à un ou plusieurs endroits de l'interface utilisateur. En fait, vous pouvez combiner les deux approches et avoir à la fois une fetchCounterbarre de progression globale pour certaines en haut de l'écran et plusieurs isFetchingindicateurs spécifiques pour les listes et les pages.
Dan Abramov le
Si j'ai des requêtes POST dans plus d'un fichier, comment définirais-je l'état de isFetching pour garder une trace de son état actuel?
user989988
13

J'aimerais ajouter quelque chose. L'exemple du monde réel utilise un champ isFetchingdans le magasin pour représenter le moment où une collection d'éléments est extraite. Toute collection est généralisée à un paginationréducteur qui peut être connecté à vos composants pour suivre l'état et montrer si une collection est en cours de chargement.

Il m'est arrivé que je voulais récupérer les détails d'une entité spécifique qui ne rentre pas dans le modèle de pagination. Je voulais avoir un état représentant si les détails sont récupérés sur le serveur, mais je ne voulais pas non plus avoir de réducteur juste pour cela.

Pour résoudre ce problème, j'ai ajouté un autre réducteur générique appelé fetching. Il fonctionne de la même manière que le réducteur de pagination et sa responsabilité est simplement de regarder un ensemble d'actions et de générer un nouvel état avec des paires [entity, isFetching]. Cela permet au connectréducteur de n'importe quel composant et de savoir si l'application récupère actuellement des données non seulement pour une collection mais pour une entité spécifique.

Javivelasco
la source
2
Merci d'avoir répondu! La gestion du chargement des éléments individuels et de leur statut est rarement discutée!
Gilad Peleg
Quand j'ai un composant qui dépend de l'action d'un autre, une solution rapide et sale est dans votre mapStateToProps, combinez-les comme ceci: isFetching: posts.isFetching || comments.isFetching - vous pouvez désormais bloquer l'interaction utilisateur pour les deux composants lorsque l'un ou l'autre est en cours de mise à jour.
Philip Murphy
5

Je ne suis pas tombé sur cette question jusqu'à présent, mais comme aucune réponse n'est acceptée, je vais jeter mon chapeau. J'ai écrit un outil pour ce travail: react-loader-factory . Il y a un peu plus de travail que la solution d'Abramov, mais il est plus modulaire et pratique, car je ne voulais pas avoir à réfléchir après l'avoir écrit.

Il y a quatre gros morceaux:

  • Modèle d'usine: Cela vous permet d'appeler rapidement la même fonction pour définir quels états signifient "Chargement" pour votre composant et quelles actions envoyer. (Cela suppose que le composant est responsable du démarrage des actions qu'il attend.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: Le composant produit par l'usine est un "composant d'ordre supérieur" (comme ce qui connect()revient dans Redux), de sorte que vous pouvez simplement le boulonner sur votre matériel existant.const LoadingChild = loaderWrapper(ChildComponent);
  • Interaction Action / Réducteur: le wrapper vérifie si un réducteur auquel il est branché contient des mots-clés qui lui disent de ne pas passer par le composant qui a besoin de données. Les actions distribuées par le wrapper devraient produire les mots-clés associés (la façon dont redux-api-middleware distribue ACTION_SUCCESSet ACTION_REQUEST, par exemple). (Vous pouvez envoyer des actions ailleurs et simplement surveiller depuis le wrapper si vous le souhaitez, bien sûr.)
  • Throbber: le composant que vous souhaitez voir apparaître alors que les données dont dépend votre composant ne sont pas prêts. J'ai ajouté un petit div pour que vous puissiez le tester sans avoir à le configurer.

Le module lui-même est indépendant de redux-api-middleware, mais c'est avec cela que je l'utilise, alors voici un exemple de code du README:

Un composant avec un chargeur l'enveloppant:

import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory';
import ChildComponent from './ChildComponent';

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);

const LoadingChild = loaderWrapper(ChildComponent);

const containingComponent = props => {
  // Do whatever you need to do with your usual containing component 

  const childProps = { someProps: 'props' };

  return <LoadingChild { ...childProps } />;
}

Un réducteur que le chargeur doit surveiller (bien que vous puissiez le câbler différemment si vous le souhaitez):

export function activeRequests(state = [], action) {
  const newState = state.slice();

  // regex that tests for an API action string ending with _REQUEST 
  const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
  // regex that tests for a API action string ending with _SUCCESS 
  const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);

  // if a _REQUEST comes in, add it to the activeRequests list 
  if (reqReg.test(action.type)) {
    newState.push(action.type);
  }

  // if a _SUCCESS comes in, delete its corresponding _REQUEST 
  if (sucReg.test(action.type)) {
    const reqType = action.type.split('_')[0].concat('_REQUEST');
    const deleteInd = state.indexOf(reqType);

    if (deleteInd !== -1) {
      newState.splice(deleteInd, 1);
    }
  }

  return newState;
}

Je pense que dans un proche avenir, j'ajouterai des éléments tels que le délai d'expiration et l'erreur au module, mais le modèle ne sera pas très différent.


La réponse courte à votre question est:

  1. Liez le rendu au code de rendu - utilisez un wrapper autour du composant dont vous avez besoin pour effectuer le rendu avec les données comme celle que j'ai montrée ci-dessus.
  2. Ajoutez un réducteur qui rend le statut des demandes autour de l'application qui pourrait vous intéresser facilement digeste, pour ne pas avoir à réfléchir trop à ce qui se passe.
  3. Les événements et l'état ne sont pas vraiment différents.
  4. Le reste de vos intuitions me semble correct.
étoile brillante
la source
4

Suis-je le seul à penser que les indicateurs de chargement n'appartiennent pas à un magasin Redux? Je veux dire, je ne pense pas que cela fasse partie de l'état d'une application en soi.

Maintenant, je travaille avec Angular2, et ce que je fais, c'est que j'ai un service "Loading" qui expose différents indicateurs de chargement via RxJS BehaviourSubjects .. Je suppose que le mécanisme est le même, je ne stocke tout simplement pas les informations dans Redux.

Les utilisateurs de LoadingService s'abonnent simplement aux événements qu'ils souhaitent écouter.

Mes créateurs d'action Redux appellent le LoadingService chaque fois que les choses doivent changer. Les composants UX s'abonnent aux observables exposés ...

Spock
la source
c'est pourquoi j'aime l'idée de magasin, où toutes les actions peuvent être interrogées (ngrx et redux-logic), le service n'est pas fonctionnel, redux-logic - fonctionnel.
Bonne
20
Salut, je reviens plus d'un an après, juste pour dire que je me suis trompé. Bien sûr, l'état UX appartient à l'état de l'application. Comment pourrais-je être stupide?
Spock
3

Vous pouvez ajouter des écouteurs de changement à vos magasins, à l'aide connect()de React Redux ou de la store.subscribe()méthode de bas niveau . Vous devriez avoir l'indicateur de chargement dans votre magasin, que le gestionnaire de changement de magasin peut ensuite vérifier et mettre à jour l'état du composant. Le composant restitue ensuite le préchargeur si nécessaire, en fonction de l'état.

alertet confirmne devrait pas être un problème. Ils bloquent et alert ne prend même aucune entrée de l'utilisateur. Avec confirm, vous pouvez définir l'état en fonction de ce sur quoi l'utilisateur a cliqué si le choix de l'utilisateur doit affecter le rendu des composants. Sinon, vous pouvez stocker le choix en tant que variable membre du composant pour une utilisation ultérieure.

Miloš Rašić
la source
sur le code d'alerte / de confirmation, où doivent-ils être placés, des actions ou des réducteurs?
企业 应用 架构 模式 大师
Cela dépend de ce que vous voulez en faire, mais honnêtement, je les mettrais dans le code du composant dans la plupart des cas, car ils font partie de l'interface utilisateur, pas de la couche de données.
Miloš Rašić
certains composants de l'interface utilisateur agissent en déclenchant un événement (événement de changement d'état) au lieu du statut lui-même. Comme une animation, montrant / masquant le préchargeur. Comment les traitez-vous?
企业 应用 架构 模式 大师
Si vous souhaitez utiliser un composant non réactif dans votre application react, la solution généralement utilisée consiste à faire réagir un composant wrapper, puis à utiliser ses méthodes de cycle de vie pour initialiser, mettre à jour et détruire une instance du composant non réactif. La plupart de ces composants utilisent des éléments d'espace réservé dans le DOM pour s'initialiser, et vous les rendriez dans la méthode de rendu du composant react. Vous pouvez en savoir plus sur les méthodes de cycle de vie ici: facebook.github.io/react/docs/component-specs.html
Miloš Rašić
J'ai un cas: une zone de notification dans le coin supérieur droit, qui contient un message de notification, chaque message apparaît puis disparaît au bout de 5 secondes. Ce composant est hors de la vue Web, fourni par l'application native de l'hôte. Il fournit une interface js telle que addNofication(message). Un autre cas est celui des préchargeurs qui sont également fournis par l'application native de l'hôte et déclenchés par son API javascript. J'ajoute un wrapper pour ces api, dans componentDidUpdateun composant React. Comment concevoir les accessoires ou l'état de ce composant?
企业 应用 架构 模式 大师
3

Nous avons trois types de notifications dans notre application, qui sont toutes conçues comme des aspects:

  1. Indicateur de chargement (modal ou non modal basé sur l'accessoire)
  2. Fenêtre contextuelle d'erreur (modale)
  3. Snack-bar de notification (non modal, à fermeture automatique)

Tous les trois se trouvent au niveau supérieur de notre application (principale) et sont câblés via Redux, comme indiqué dans l'extrait de code ci-dessous. Ces accessoires contrôlent l'affichage de leurs aspects correspondants.

J'ai conçu un proxy qui gère tous nos appels d'API, ainsi toutes les erreurs isFetching et (api) sont médiatisées avec actionCreators que j'importe dans le proxy. (En passant, j'utilise également webpack pour injecter une maquette du service de support pour les développeurs afin que nous puissions travailler sans dépendances de serveur.)

Tout autre endroit de l'application qui doit fournir un type de notification importe simplement l'action appropriée. Snackbar & Error ont des paramètres pour les messages à afficher.

@connect(
// map state to props
state => ({
    isFetching      :state.main.get('isFetching'),   // ProgressIndicator
    notification    :state.main.get('notification'), // Snackbar
    error           :state.main.get('error')         // ErrorPopup
}),
// mapDispatchToProps
(dispatch) => { return {
    actions: bindActionCreators(actionCreators, dispatch)
}}

) exporter la classe par défaut Main étend React.Component {

Dreculah
la source
Je travaille sur une configuration similaire avec l'affichage d'un chargeur / notifications. Je rencontre des problèmes; auriez-vous un aperçu ou un exemple de la façon dont vous accomplissez ces tâches?
Aymen
2

J'enregistre les URL telles que ::

isFetching: {
    /api/posts/1: true,
    api/posts/3: false,
    api/search?q=322: true,
}

Et puis j'ai un sélecteur mémorisé (via reselect).

const getIsFetching = createSelector(
    state => state.isFetching,
    items => items => Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false
);

Pour rendre l'url unique en cas de POST, je passe une variable comme requête.

Et là où je veux afficher un indicateur, j'utilise simplement la variable getFetchCount

Sergiu
la source
1
Vous pouvez remplacer Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsepar Object.keys(items).every(item => items[item])en passant.
Alexandre Annic
1
Je pense que vous vouliez dire someau lieu de every, mais oui, trop de comparaisons non nécessaires dans la première solution proposée. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena