web-dev-qa-db-fra.com

Comment afficher un indicateur de chargement dans React Application Redux lors de la récupération des données?

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 pas être résolus par moi-même.

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

Donc, mon premier cas montre/cache un préchargeur lors de la récupération des API. Le middleware enverra une action de la FSA au début et une action de la FSA à la fin. Les deux actions sont reçues par les réducteurs qui ne devraient effectuer que certains traitements de données normaux. Pas d'opérations d'interface utilisateur, plus d'opérations. Peut-être devrais-je sauvegarder l'état de traitement en état puis les rendre lors de la mise à jour du magasin.

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

Pire encore, que dois-je faire si je dois utiliser le dialogue de confirmation natif ou le dialogue d'alerte dans les applications redux/react? Où devraient-ils être placés, actions ou réducteurs?

Meilleurs vœux! Souhait de répondre.

Je veux dire qu'ils sont plus comme des événements que des états!

Je ne le dirais pas. Je pense que les indicateurs de charge sont un excellent cas d’UI qui est facilement décrit en fonction de l’état: dans ce cas, d’une variable booléenne. Bien que cette réponse soit correcte, je voudrais fournir un code pour l’accompagner.

Dans le async exemple dans le référentiel Redux , 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 renvoie isFetching dans le cadre de la valeur mapStateToProps() de retour est 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 tilise isFetching prop dans la fonction render() pour générer un libellé "Loading ..." (qui pourrait éventuellement être une visière):

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

Pire encore, que dois-je faire si je dois utiliser le dialogue de confirmation natif ou le dialogue d'alerte dans les applications redux/react? Où devraient-ils être placés, actions ou réducteurs?

Tous les effets secondaires (et afficher un dialogue est très certainement un effet secondaire) n'appartiennent pas aux réducteurs. Considérez les réducteurs comme des "bâtisseurs d’État" passifs. Ils ne font pas vraiment des 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 y appliquer des effets secondaires.

Pour chaque règle, il y a une exception. Parfois, la logique de vos effets secondaires est tellement compliquée que vous souhaitez les associer 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 le faites que lorsque vous êtes à l’aise avec Vanilla Redux et que vous avez un réel problème d’effets secondaires épars que vous aimeriez rendre plus gérable.

147
Dan Abramov

Excellente réponse Dan Abramov! Je veux juste ajouter que je faisais plus ou moins exactement cela dans une de mes applications (conserver isFetching en tant que booléen) et que je devais en faire un entier (qui finit par lire comme le nombre de demandes en attente) pour prendre en charge plusieurs messages simultanés. demandes.

avec booléen:

demande 1 commence -> spinner sur -> demande 2 commence -> demande 1 se termine -> spinner off -> demande 2 se termine

avec entier:

demande 1 commence -> spinner on -> demande 2 commence -> 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
19
Nuno Campos

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

Il m'est arrivé de chercher des détails pour une entité spécifique qui ne correspond pas au modèle de pagination. Je voulais avoir un état représentant si les détails sont extraits du serveur, mais je ne voulais pas non plus avoir un réducteur juste pour cela.

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

13
javivelasco

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

Il y a quatre grandes pièces:

  • Modèle d'usine: Cela vous permet d'appeler rapidement la même fonction pour définir les états qui signifient "Chargement" pour votre composant et les 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 la fabrique est un "composant d'ordre supérieur" (comme ce que connect() renvoie dans Redux), afin que vous puissiez simplement le boulonner sur votre matériel existant. const LoadingChild = loaderWrapper(ChildComponent);
  • Interaction Action/Réducteur: l'encapsuleur vérifie si un réducteur auquel il est connecté contient des mots-clés lui interdisant de transmettre des informations au composant nécessitant des données. Les actions envoyées par l'encapsuleur sont censées produire les mots-clés associés (la manière dont redux-api-middleware distribue ACTION_SUCCESS et ACTION_REQUEST, par exemple). (Vous pouvez bien sûr envoyer des actions ailleurs et simplement surveiller à partir du wrapper.)
  • Throbber: le composant que vous souhaitez afficher alors que les données dont votre composant dépend n'est pas prêt. J'ai ajouté un petit div dedans afin que vous puissiez le tester sans avoir à le gréer.

Le module lui-même est indépendant du middleware redux-api, mais c'est avec cela que je l'utilise, voici donc un exemple de code tiré du fichier 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 à surveiller par le chargeur (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 avenir proche, j'ajouterai des éléments tels que timeout et error au module, mais le modèle ne sera pas très différent.


La réponse courte à votre question est la suivante:

  1. Lier le rendu au code de rendu - utilisez un wrapper autour du composant dont vous avez besoin pour 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 dans l'application qui vous tient à coeur facilement assimilable, afin que vous ne pensiez pas 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.
5
bright-star

Suis-je le seul à penser que les indicateurs de charge n'appartiennent pas à un magasin Redux? Je veux dire, je ne pense pas que cela fait 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 pas les informations dans Redux.

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

Les créateurs de mes actions Redux appellent le LoadingService chaque fois que des modifications sont nécessaires. Les composants UX souscrivent aux observables exposés ...

3
Spock

Nous avons trois types de notifications dans notre application, qui sont tous conçus en tant qu'aspects:

  1. Indicateur de chargement (modal ou non modal basé sur un accessoire)
  2. Erreur Popup (modal)
  3. Notification snackbar (non modal, fermeture automatique)

Ces trois éléments sont au niveau supérieur de notre application (principal) et 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 transmises à actionCreators que j'importe dans le proxy. (En passant, j'utilise également webpack pour injecter une maquette du service de support pour dev afin que nous puissions travailler sans dépendances du serveur.)

Tout autre endroit de l'application qui doit fournir n'importe quel 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)
}}

) la classe d'exportation par défaut principale s'étend React.Component {

3
Dreculah

Vous pouvez ajouter des écouteurs de modification à vos magasins en utilisant la méthode connect() de React Redux ou la méthode de bas niveau store.subscribe(). Vous devez 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.

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

3
Miloš Rašić

Je sauve les URL tels 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 resélectionner).

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 en tant que requête.

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

2
Sergiu