Come mostrare un indicatore di caricamento nell'app React Redux durante il recupero dei dati? [chiuso]


107

Sono nuovo in React / Redux. Uso un middleware api di recupero nell'app Redux per elaborare le API. È ( redux-api-middleware ). Penso che sia il modo migliore per elaborare azioni API asincrone. Ma trovo alcuni casi che non possono essere risolti da solo.

Come dice la homepage ( Lifecycle ), un ciclo di vita dell'API di recupero inizia con l'invio di un'azione CALL_API e termina con l'invio di un'azione FSA.

Quindi il mio primo caso è mostrare / nascondere un preloader durante il recupero delle API. Il middleware invierà un'azione FSA all'inizio e alla fine invierà un'azione FSA. Entrambe le azioni vengono ricevute da riduttori che dovrebbero eseguire solo una normale elaborazione dei dati. Nessuna operazione dell'interfaccia utente, non più operazioni. Forse dovrei salvare lo stato di elaborazione nello stato, quindi renderli durante l'aggiornamento del negozio.

Ma come farlo? Un flusso di componenti di reazione su tutta la pagina? cosa succede con l'aggiornamento del negozio da altre azioni? Voglio dire, sono più simili a eventi che a stati!

Anche in un caso peggiore, cosa devo fare quando devo utilizzare la finestra di dialogo di conferma nativa o la finestra di dialogo di avviso nelle app redux / react? Dove vanno messi, azioni o riduzioni?

Auguri! Desiderio di rispondere.


1
È stata ripristinata l'ultima modifica a questa domanda poiché ha modificato l'intero punto della domanda e delle risposte di seguito.
Gregg B

Un evento è il cambio di stato!
企业 应用 架构 模式 大师

Dai un'occhiata a questrar. github.com/orar/questrar
Orar

Risposte:


152

Voglio dire, sono più simili a eventi che a stati!

Non lo direi. Penso che gli indicatori di caricamento siano un ottimo caso di UI che può essere facilmente descritto come una funzione di stato: in questo caso, di una variabile booleana. Sebbene questa risposta sia corretta, vorrei fornire del codice per seguirla.

Nel asyncesempio nel Redux pronti contro termine , riduttore aggiorna un campo chiamatoisFetching :

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

Il componente utilizza connect()da React Redux per iscriversi allo stato del negozio e restituisce isFetchingcome parte del mapStateToProps()valore restituito in modo che sia disponibile negli oggetti di scena del componente connesso:

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

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Infine, il componente utilizza isFetchingprop nella render()funzione per eseguire il rendering di un'etichetta "Caricamento in corso ..." (che potrebbe essere piuttosto uno spinner):

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

Anche in un caso peggiore, cosa devo fare quando devo utilizzare la finestra di dialogo di conferma nativa o la finestra di dialogo di avviso nelle app redux / react? Dove vanno messi, azioni o riduzioni?

Eventuali effetti collaterali (e mostrare una finestra di dialogo è sicuramente un effetto collaterale) non appartengono ai riduttori. Pensa ai riduttori come passivi "costruttori di stato". Non "fanno" davvero le cose.

Se desideri mostrare un avviso, fallo da un componente prima di inviare un'azione o fallo da un creatore di azioni. Nel momento in cui un'azione viene eseguita, è troppo tardi per eseguire gli effetti collaterali in risposta ad essa.

Per ogni regola c'è un'eccezione. A volte la logica degli effetti collaterali è così complicata che in realtà vuoi accoppiarli a tipi di azione specifici oa riduttori specifici. In questo caso controlla progetti avanzati come Redux Saga e Redux Loop . Fallo solo quando sei a tuo agio con Vanilla Redux e hai un vero problema di effetti collaterali sparsi che vorresti rendere più gestibili.


16
E se devo eseguire più recuperi? Quindi una variabile non sarebbe sufficiente.
philk

1
@philk se hai più recuperi puoi raggrupparli Promise.allin una singola promessa e quindi inviare una singola azione per tutti i recuperi. Oppure devi mantenere più isFetchingvariabili nel tuo stato.
Sebastien Lorber

2
Si prega di esaminare attentamente l'esempio a cui mi collego. C'è più di una isFetchingbandiera. È impostato per ogni set di oggetti che viene recuperato. Puoi usare la composizione del riduttore per implementarlo.
Dan Abramov

3
Tieni presente che se la richiesta fallisce e RECEIVE_POSTSnon viene mai attivata, il segno di caricamento rimarrà in posizione a meno che tu non abbia creato una sorta di timeout per mostrare un error loadingmessaggio.
James111

2
@TomiS - Inserisco esplicitamente nella blacklist tutte le mie proprietà isFetching da qualsiasi persistenza redux che sto usando.
duhseekoh

22

Ottima risposta Dan Abramov! Voglio solo aggiungere che stavo facendo più o meno esattamente quello in una delle mie app (mantenendo isFetching come booleano) e ho finito per renderlo un numero intero (che finisce per leggere come il numero di richieste in sospeso) per supportare più richieste simultanee richieste.

con booleano:

richiesta 1 avvia -> spinner acceso -> richiesta 2 avvia -> richiesta 1 termina -> spinner spento -> richiesta 2 termina

con numero intero:

richiesta 1 inizia -> spinner acceso -> richiesta 2 inizia -> richiesta 1 termina -> richiesta 2 termina -> spinner spento

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

2
Questo è ragionevole. Tuttavia il più delle volte si desidera memorizzare anche alcuni dati recuperati oltre al flag. A questo punto dovrai avere più di un oggetto con isFetchingflag. Se osservi attentamente l'esempio che ti ho collegato vedrai che non c'è un oggetto con isFetchedma molti: uno per subreddit (che è ciò che viene recuperato in quell'esempio).
Dan Abramov,

2
Oh. sì, non l'ho notato. tuttavia nel mio caso ho una voce globale isFetching nello stato e una voce cache in cui sono archiviati i dati recuperati, e per i miei scopi mi interessa solo che si stia verificando qualche attività di rete, non importa per cosa
Nuno Campos

4
Sì! Dipende se desideri mostrare l'indicatore di recupero in uno o più punti dell'interfaccia utente. In effetti puoi combinare i due approcci e avere sia un globale fetchCounterper qualche barra di avanzamento nella parte superiore dello schermo che diversi isFetchingflag specifici per elenchi e pagine.
Dan Abramov,

Se ho richieste POST in più di un file, come potrei impostare lo stato di isFetching per tenere traccia del suo stato corrente?
user989988

13

Vorrei aggiungere qualcosa. L'esempio del mondo reale utilizza un campo isFetchingnel negozio per rappresentare quando viene recuperata una raccolta di elementi. Qualsiasi raccolta è generalizzata a un filepagination riduttore che può essere collegato ai componenti per tenere traccia dello stato e mostrare se una raccolta è in fase di caricamento.

Mi è capitato di voler recuperare i dettagli per un'entità specifica che non si adatta allo schema di impaginazione. Volevo avere uno stato che rappresentasse se i dettagli vengono recuperati dal server, ma non volevo anche avere un riduttore solo per quello.

Per risolvere questo problema ho aggiunto un altro riduttore generico chiamato fetching. Funziona in modo simile al riduttore di paginazione ed è responsabilità solo osservare un insieme di azioni e generare un nuovo stato con le coppie [entity, isFetching]. Ciò consente al connectriduttore di qualsiasi componente e di sapere se l'app sta attualmente recuperando i dati non solo per una raccolta ma per un'entità specifica.


2
Grazie per la risposta! La gestione del caricamento di singoli articoli e il loro stato è raramente discussa!
Gilad Peleg

Quando ho un componente che dipende dall'azione di un altro, una rapida e sporca via d'uscita è nel tuo mapStateToProps combinandoli in questo modo: isFetching: posts.isFetching || comments.isFetching: ora puoi bloccare l'interazione dell'utente per entrambi i componenti quando uno dei due viene aggiornato.
Philip Murphy

5

Questa domanda non mi è mai capitata fino ad ora, ma poiché nessuna risposta è accettata, mi metterò il cappello. Ho scritto uno strumento proprio per questo lavoro: react-loader-factory . Ha un po 'più di successo rispetto alla soluzione di Abramov, ma è più modulare e conveniente, dal momento che non volevo dover pensare dopo averlo scritto.

Ci sono quattro pezzi grandi:

  • Modello di fabbrica: questo ti permette di chiamare rapidamente la stessa funzione per impostare quali stati significano "Caricamento" per il tuo componente e quali azioni inviare. (Ciò presuppone che il componente sia responsabile dell'avvio delle azioni su cui attende.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: Il componente che Factory produce è un "componente di ordine superiore" (come quello che connect()ritorna in Redux), in modo che tu possa semplicemente fissarlo alle tue cose esistenti.const LoadingChild = loaderWrapper(ChildComponent);
  • Interazione azione / riduttore: il wrapper verifica se un riduttore a cui è collegato contiene parole chiave che gli dicono di non passare al componente che necessita di dati. Le azioni inviate dal wrapper dovrebbero produrre le parole chiave associate (il modo in cui redux-api-middleware invia ACTION_SUCCESSe ACTION_REQUEST, ad esempio). (Puoi inviare azioni altrove e monitorare semplicemente dal wrapper se lo desideri, ovviamente.)
  • Throbber: il componente che desideri venga visualizzato mentre i dati da cui dipende il tuo componente non sono pronti. Ho aggiunto un piccolo div in modo che tu possa provarlo senza doverlo montare.

Il modulo stesso è indipendente da redux-api-middleware, ma è quello con cui lo uso, quindi ecco un po 'di codice di esempio dal README:

Un componente con un caricatore che lo avvolge:

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 riduttore per il monitoraggio del caricatore (anche se puoi cablarlo in modo diverso se lo desideri):

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;
}

Mi aspetto che nel prossimo futuro aggiungerò cose come timeout ed errore al modulo, ma il modello non sarà molto diverso.


La risposta breve alla tua domanda è:

  1. Collega il rendering al codice di rendering: usa un wrapper attorno al componente di cui hai bisogno per il rendering con i dati come quello che ho mostrato sopra.
  2. Aggiungi un riduttore che renda facilmente digeribile lo stato delle richieste relative all'app che ti interessano, in modo da non dover pensare troppo a ciò che sta accadendo.
  3. Gli eventi e lo stato non sono molto diversi.
  4. Il resto delle tue intuizioni mi sembrano corrette.

4

Sono l'unico a pensare che gli indicatori di caricamento non appartengano a un negozio Redux? Voglio dire, non penso che faccia parte dello stato di un'applicazione di per sé ..

Ora, lavoro con Angular2, e quello che faccio è avere un servizio di "caricamento" che espone diversi indicatori di caricamento tramite RxJS BehaviourSubjects .. Suppongo che il meccanismo sia lo stesso, semplicemente non memorizzo le informazioni in Redux.

Gli utenti del LoadingService devono semplicemente iscriversi a quegli eventi che vogliono ascoltare.

I miei creatori di azioni Redux chiamano il LoadingService ogni volta che le cose devono cambiare. I componenti UX si iscrivono alle osservabili esposte ...


questo è il motivo per cui mi piace l'idea di store, dove tutte le azioni possono essere interrogate (ngrx e redux-logic), il servizio non è funzionale, redux-logic - funzionale. Bella lettura
srghma

20
Ciao, ricontrollo più di un anno dopo, solo per dire che mi sbagliavo di grosso. Ovviamente lo stato UX appartiene allo stato dell'applicazione. Quanto potrei essere stupido?
Spock

3

Puoi aggiungere listener di modifiche ai tuoi negozi, utilizzando connect()React Redux o il store.subscribe()metodo di basso livello . Dovresti avere l'indicatore di caricamento nel tuo negozio, che il gestore delle modifiche del negozio può quindi controllare e aggiornare lo stato del componente. Il componente quindi esegue il rendering del preloader, se necessario, in base allo stato.

alerte confirmnon dovrebbe essere un problema. Si stanno bloccando e l'avviso non riceve nemmeno alcun input dall'utente. Con confirm, è possibile impostare lo stato in base a ciò su cui l'utente ha fatto clic se la scelta dell'utente deve influire sul rendering dei componenti. In caso contrario, è possibile memorizzare la scelta come variabile membro componente per un uso successivo.


sul codice di avviso / conferma, dove devono essere inseriti, azioni o riduzioni?
企业 应用 架构 模式 大师

Dipende da cosa vuoi fare con loro, ma onestamente li metterei nel codice del componente nella maggior parte dei casi poiché fanno parte dell'interfaccia utente, non del livello dati.
Miloš Rašić

alcuni componenti dell'interfaccia utente agiscono attivando un evento (evento di modifica dello stato) invece dello stato stesso. Come un'animazione, che mostra / nasconde il precaricatore. Come li elabori?
企业 应用 架构 模式 大师

Se si desidera utilizzare un componente che non reagisce nella propria app, la soluzione generalmente utilizzata è creare un componente di reazione del wrapper, quindi utilizzare i suoi metodi del ciclo di vita per inizializzare, aggiornare e distruggere un'istanza del componente che non reagisce. La maggior parte di questi componenti utilizza elementi segnaposto nel DOM per l'inizializzazione e tu renderesti quelli nel metodo di rendering del componente reattivo. Puoi leggere di più sui metodi del ciclo di vita qui: facebook.github.io/react/docs/component-specs.html
Miloš Rašić

Ho un caso: un'area di notifica nell'angolo in alto a destra, che contiene un messaggio di notifica, ogni messaggio viene visualizzato e poi scompare dopo 5 secondi. Questo componente è fuori dalla visualizzazione web, fornito dall'app nativa dell'host. Fornisce alcune interfacce js come addNofication(message). Un altro caso sono i preloader che vengono forniti anche dall'app nativa dell'host e attivati ​​dalla sua API JavaScript. Aggiungo un wrapper per quelle API, in componentDidUpdateun componente React. Come si progettano gli oggetti di scena o lo stato di questo componente?
企业 应用 架构 模式 大师

3

Abbiamo tre tipi di notifiche nella nostra app, tutte progettate come aspetti:

  1. Indicatore di carico (modale o non modale in base al puntello)
  2. Popup di errore (modale)
  3. Notifica snackbar (non modale, a chiusura automatica)

Tutti e tre si trovano al livello più alto della nostra app (Principale) e cablati tramite Redux come mostrato nello snippet di codice sottostante. Questi oggetti di scena controllano la visualizzazione dei loro aspetti corrispondenti.

Ho progettato un proxy che gestisce tutte le nostre chiamate API, quindi tutti gli errori isFetching e (api) sono mediati con actionCreators che importano nel proxy. (Per inciso, uso anche webpack per iniettare una simulazione del servizio di supporto per dev in modo da poter lavorare senza dipendenze dal server.)

Qualsiasi altro punto nell'app che deve fornire qualsiasi tipo di notifica importa semplicemente l'azione appropriata. Snackbar & Error hanno parametri per la visualizzazione dei messaggi.

@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)
}}

) export default class Main estende React.Component {


Sto lavorando a una configurazione simile con la visualizzazione di un caricatore / notifiche. Sto riscontrando problemi; avresti un'idea o un esempio di come svolgi questi compiti?
Aymen

2

Sto salvando gli URL come:

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

E poi ho un selettore memorizzato (tramite reselect).

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

Per rendere l'url univoco in caso di POST, passo alcune variabili come query.

E dove voglio mostrare un indicatore, uso semplicemente la variabile getFetchCount


1
Puoi sostituire Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsea Object.keys(items).every(item => items[item])proposito.
Alexandre Annic

1
Penso che intendevi someinvece di every, ma sì, troppi confronti non necessari nella prima soluzione proposta. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.