Perché abbiamo bisogno del middleware per il flusso asincrono in Redux?


687

Secondo i documenti, "Senza middleware, l'archivio Redux supporta solo il flusso di dati sincrono" . Non capisco perché sia ​​così. Perché il componente contenitore non può chiamare l'API asincrona e quindi dispatchle azioni?

Ad esempio, immagina una semplice interfaccia utente: un campo e un pulsante. Quando l'utente preme il pulsante, il campo viene popolato con i dati di un server remoto.

Un campo e un pulsante

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

Quando viene eseguito il rendering del componente esportato, posso fare clic sul pulsante e l'input viene aggiornato correttamente.

Nota la updatefunzione nella connectchiamata. Invia un'azione che comunica all'app che si sta aggiornando e quindi esegue una chiamata asincrona. Al termine della chiamata, il valore fornito viene inviato come payload di un'altra azione.

Cosa c'è di sbagliato in questo approccio? Perché dovrei usare Redux Thunk o Redux Promise, come suggerisce la documentazione?

EDIT: ho cercato indizi nel repository Redux e ho scoperto che in passato i creatori di azioni dovevano essere funzioni pure. Ad esempio, ecco un utente che sta cercando di fornire una spiegazione migliore per il flusso di dati asincrono:

Lo stesso creatore di azioni è ancora una funzione pura, ma la funzione thunk che restituisce non ha bisogno di essere, e può fare le nostre chiamate asincrone

I creatori di azioni non devono più essere puri. Quindi, il middleware thunk / promise era sicuramente necessario in passato, ma sembra che non sia più così?


53
Ai creatori di azioni non è mai stato richiesto di essere funzioni pure. È stato un errore nei documenti, non una decisione che è cambiata.
Dan Abramov,

1
@DanAbramov per testabilità può essere comunque una buona pratica. Redux-saga lo consente: stackoverflow.com/a/34623840/82609
Sebastien Lorber

Risposte:


702

Cosa c'è di sbagliato in questo approccio? Perché dovrei usare Redux Thunk o Redux Promise, come suggerisce la documentazione?

Non c'è niente di sbagliato in questo approccio. È semplicemente scomodo in un'applicazione di grandi dimensioni perché avrai componenti diversi che eseguono le stesse azioni, potresti voler rimbalzare alcune azioni o mantenere uno stato locale come gli ID a incremento automatico vicino ai creatori di azioni, ecc. Quindi è più semplice il punto di vista della manutenzione per estrarre i creatori di azioni in funzioni separate.

Puoi leggere la mia risposta a "Come inviare un'azione Redux con un timeout" per una procedura dettagliata più dettagliata.

Middleware come Redux Thunk o Redux Promessa appena ti dà “lo zucchero sintassi” per il dispacciamento thunk o promesse, ma non si deve usarlo.

Quindi, senza alcun middleware, potrebbe apparire il tuo creatore di azioni

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

Ma con Thunk Middleware puoi scriverlo in questo modo:

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

Quindi non c'è alcuna differenza enorme. Una cosa che mi piace di quest'ultimo approccio è che al componente non importa che il creatore di azioni sia asincrono. Si chiama semplicemente dispatchnormalmente, può anche essere utilizzato mapDispatchToPropsper associare tale creatore di azioni con una breve sintassi, ecc. I componenti non sanno come vengono implementati i creatori di azioni e puoi passare tra diversi approcci asincroni (Redux Thunk, Redux Promise, Redux Saga ) senza cambiare i componenti. D'altra parte, con il primo approccio esplicito, i componenti sanno esattamente che una chiamata specifica è asincrona e deve dispatchessere passata da una convenzione (ad esempio, come parametro di sincronizzazione).

Pensa anche a come cambierà questo codice. Supponiamo di voler avere una seconda funzione di caricamento dei dati e di combinarli in un unico creatore di azioni.

Con il primo approccio dobbiamo essere consapevoli del tipo di creatore di azioni che stiamo chiamando:

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

Con Redux Thunk i creatori di azioni possono essere dispatchil risultato di altri creatori di azioni e nemmeno pensare che siano sincroni o asincroni:

// 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!
}

Con questo approccio, se in seguito desideri che i tuoi creatori di azioni esaminino l'attuale stato Redux, puoi semplicemente utilizzare il secondo getStateargomento passato ai thunk senza modificare affatto il codice chiamante:

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

Se è necessario modificarlo in modo sincrono, è possibile farlo anche senza modificare alcun codice di chiamata:

// 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')
  }
}

Quindi il vantaggio di usare middleware come Redux Thunk o Redux Promise è che i componenti non sono consapevoli di come sono implementati i creatori di azioni e se si preoccupano dello stato di Redux, se sono sincroni o asincroni e se chiamano o meno altri creatori di azioni . L'aspetto negativo è un po 'indiretto, ma crediamo che ne valga la pena in applicazioni reali.

Infine, Redux Thunk and friends è solo un possibile approccio alle richieste asincrone nelle app Redux. Un altro approccio interessante è Redux Saga che ti consente di definire demoni di lunga durata ("saghe") che intraprendono azioni come vengono e trasformano o eseguono richieste prima di emettere azioni. Questo sposta la logica dai creatori di azioni in saghe. Potresti voler dare un'occhiata e in seguito scegliere quello che più ti si addice.

Ho cercato indizi sul repository Redux e ho scoperto che in passato i creatori di azioni dovevano essere funzioni pure.

Questo non è corretto I documenti hanno detto questo, ma i documenti erano sbagliati.
Ai creatori di azioni non è mai stato richiesto di essere funzioni pure.
Abbiamo corretto i documenti per riflettere ciò.


57
Forse un modo breve per dire che il pensiero di Dan è: il middleware è un approccio centralizzato, in questo modo ti consente di mantenere i tuoi componenti più semplici e generalizzati e di controllare il flusso di dati in un unico posto. Se mantieni una grande app ti piacerà =)
Sergey Lapin il

3
@asdfasdfads Non vedo perché non funzioni. Funzionerebbe esattamente allo stesso modo; messo alertdopo dispatch()l'azione.
Dan Abramov,

9
Penultima riga nel primo esempio di codice: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Perché devo passare in spedizione? Se per convenzione esiste un solo negozio globale, perché non faccio semplicemente riferimento a quello direttamente e lo faccio store.dispatchogni volta che ho bisogno, ad esempio, di entrare loadData?
Søren Debois,

10
@ SørenDebois Se l'app fosse solo lato client, funzionerebbe. Se viene eseguito il rendering sul server, ti consigliamo di avere storeun'istanza diversa per ogni richiesta, quindi non puoi definirla in anticipo.
Dan Abramov,

3
Voglio solo sottolineare che questa risposta ha 139 righe che sono 9,92 volte più del codice sorgente di redux-thunk che consiste di 14 righe: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy

447

Non

Ma ... dovresti usare redux-saga :)

La risposta di Dan Abramov è giusta, redux-thunkma parlerò un po 'di più sulla saga di Redux che è abbastanza simile ma più potente.

Imperativo VS dichiarativo

  • DOM : jQuery è indispensabile / React è dichiarativo
  • Monadi : IO è un imperativo / Free è dichiarativo
  • Effetti redux : redux-thunkè indispensabile / redux-sagaè dichiarativo

Quando hai un thunk in mano, come una monade IO o una promessa, non puoi facilmente sapere cosa farà una volta eseguito. L'unico modo per testare un thunk è eseguirlo e deridere il dispatcher (o l'intero mondo esterno se interagisce con più cose ...).

Se stai usando simulazioni, allora non stai facendo una programmazione funzionale.

Visto attraverso la lente degli effetti collaterali, le beffe sono una bandiera che indica che il tuo codice è impuro e, agli occhi del programmatore funzionale, prova che qualcosa non va. Invece di scaricare una libreria per aiutarci a verificare che l'iceberg sia intatto, dovremmo navigare attorno ad esso. Un ragazzo hardcore TDD / Java una volta mi ha chiesto come si fa a deridere Clojure. La risposta è, di solito no. Di solito lo vediamo come un segno che dobbiamo riformattare il nostro codice.

fonte

Le saghe (come sono state implementate in redux-saga ) sono dichiarative e, come la monade libera o i componenti di React, sono molto più facili da testare senza alcuna derisione.

Vedi anche questo articolo :

in FP moderno, non dovremmo scrivere programmi - dovremmo scrivere descrizioni di programmi, che possiamo quindi introspettare, trasformare e interpretare a piacimento.

(In realtà, Redux-saga è come un ibrido: il flusso è imperativo ma gli effetti sono dichiarativi)

Confusione: azioni / eventi / comandi ...

C'è molta confusione nel mondo del frontend su come alcuni concetti di backend come CQRS / EventSourcing e Flux / Redux possono essere correlati, principalmente perché in Flux usiamo il termine "azione" che a volte può rappresentare sia codice imperativo ( LOAD_USER) che eventi (USER_LOADED ). Credo che, come il sourcing di eventi, si debbano inviare solo eventi.

Usare le saghe in pratica

Immagina un'app con un collegamento a un profilo utente. Il modo idiomatico di gestirlo con ciascun middleware sarebbe:

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

Questa saga si traduce in:

ogni volta che si fa clic su un nome utente, recuperare il profilo utente e quindi inviare un evento con il profilo caricato.

Come puoi vedere, ci sono alcuni vantaggi di redux-saga.

L'uso dei takeLatestpermessi per esprimere che sei interessato solo a fare clic sui dati dell'ultimo nome utente (gestisci i problemi di concorrenza nel caso in cui l'utente faccia clic molto velocemente su molti nomi utente). Questo genere di cose è difficile con i thunk. Avresti potuto usare takeEveryse non volessi questo comportamento.

Mantieni i creatori di azione puri. Nota che è ancora utile mantenere actionCreator (in saghe pute componenti dispatch), in quanto potrebbe aiutarti ad aggiungere la convalida dell'azione (asserzioni / flusso / dattiloscritto) in futuro.

Il tuo codice diventa molto più testabile in quanto gli effetti sono dichiarativi

Non hai più bisogno di innescare chiamate simili a rpc come actions.loadUser(). L'interfaccia utente deve solo inviare ciò che è successo. Spariamo solo eventi (sempre al passato!) E non più azioni. Ciò significa che è possibile creare "anatre" disaccoppiate o contesti rilegati e che la saga può fungere da punto di accoppiamento tra questi componenti modulari.

Ciò significa che le tue visualizzazioni sono più facili da gestire perché non hanno più bisogno di contenere quel livello di traduzione tra ciò che è accaduto e ciò che dovrebbe accadere come effetto

Ad esempio, immagina una vista di scorrimento infinita. CONTAINER_SCROLLEDpuò portare a NEXT_PAGE_LOADED, ma è davvero responsabilità del contenitore scorrevole decidere se caricare o meno un'altra pagina? Quindi deve essere consapevole di cose più complicate come se l'ultima pagina è stata caricata correttamente o se c'è già una pagina che tenta di caricare o se non ci sono più elementi da caricare? Non credo: per la massima riusabilità il contenitore scorrevole dovrebbe semplicemente descrivere che è stato fatto scorrere. Il caricamento di una pagina è un "effetto aziendale" di quella pergamena

Alcuni potrebbero obiettare che i generatori possono intrinsecamente nascondere lo stato al di fuori del redux store con variabili locali, ma se inizi a orchestrare cose complesse all'interno di thunk avviando timer ecc. Avresti comunque lo stesso problema. E c'è un selecteffetto che ora consente di ottenere un po 'di stato dal tuo negozio Redux.

Le saghe possono viaggiare nel tempo e abilitare anche la registrazione di flusso complessa e gli strumenti di sviluppo su cui si sta attualmente lavorando. Ecco alcuni semplici log di flusso asincrono già implementati:

registrazione del flusso di saga

Disaccoppiamento

Le saghe non stanno solo sostituendo i thunk redux. Provengono da backend / sistemi distribuiti / sourcing di eventi.

È un'idea sbagliata molto comune che le saghe siano qui solo per sostituire i thunk Redux con una migliore testabilità. In realtà questo è solo un dettaglio di implementazione di redux-saga. L'uso degli effetti dichiarativi è meglio dei thunk per testabilità, ma il modello di saga può essere implementato sopra il codice imperativo o dichiarativo.

In primo luogo, la saga è un software che consente di coordinare transazioni a lungo termine (eventuale coerenza) e transazioni in contesti diversi (gergo di progettazione guidato dal dominio).

Per semplificare questo per il mondo frontend, immagina che ci sia widget1 e widget2. Quando si fa clic su un pulsante su widget1, dovrebbe avere un effetto su widget2. Invece di accoppiare i 2 widget (ovvero widget1 invia un'azione indirizzata a widget2), widget1 invia solo che è stato fatto clic sul relativo pulsante. Quindi la saga ascolta questo pulsante facendo clic su e quindi aggiorna widget2 cancellando un nuovo evento di cui widget2 è a conoscenza.

Ciò aggiunge un livello di riferimento indiretto non necessario per le app semplici, ma semplifica il ridimensionamento di applicazioni complesse. Ora puoi pubblicare widget1 e widget2 in diversi repository npm in modo che non debbano mai conoscersi a vicenda, senza che debbano condividere un registro globale delle azioni. I 2 widget sono ora contesti limitati che possono vivere separatamente. Non hanno bisogno l'uno dell'altro per essere coerenti e possono essere riutilizzati anche in altre app. La saga è il punto di accoppiamento tra i due widget che li coordinano in modo significativo per il tuo business.

Alcuni bei articoli su come strutturare la tua app Redux, su cui puoi usare Redux-saga per motivi di disaccoppiamento:

Un caso concreto: il sistema di notifica

Voglio che i miei componenti siano in grado di attivare la visualizzazione delle notifiche in-app. Ma non voglio che i miei componenti siano altamente accoppiati al sistema di notifica che ha le sue regole aziendali (max 3 notifiche visualizzate contemporaneamente, accodamento delle notifiche, tempo di visualizzazione di 4 secondi ecc ...).

Non voglio che i miei componenti JSX decidano quando verrà mostrata / nascosta una notifica. Gli do solo la possibilità di richiedere una notifica e di lasciare le regole complesse all'interno della saga. Questo tipo di cose è abbastanza difficile da attuare con i thunk o le promesse.

notifiche

Ho descritto qui come si può fare con saga

Perché si chiama Saga?

Il termine saga viene dal mondo del backend. Inizialmente ho presentato Yassine (l'autore della saga di Redux) a quel termine in una lunga discussione .

Inizialmente, quel termine è stato introdotto con un documento , il modello di saga avrebbe dovuto essere utilizzato per gestire l'eventuale coerenza nelle transazioni distribuite, ma il suo utilizzo è stato esteso a una definizione più ampia dagli sviluppatori di backend in modo che ora copra anche il "gestore dei processi" modello (in qualche modo il modello originale della saga è una forma specializzata di gestore di processo).

Oggi, il termine "saga" è confuso in quanto può descrivere 2 cose diverse. Come viene usato in Redux-saga, non descrive un modo per gestire le transazioni distribuite ma piuttosto un modo per coordinare le azioni nella tua app. redux-sagaavrebbe anche potuto essere chiamato redux-process-manager.

Guarda anche:

alternative

Se non ti piace l'idea di usare i generatori ma sei interessato al modello di saga e alle sue proprietà di disaccoppiamento, puoi anche ottenere lo stesso con redux-osservabile che usa il nome epicper descrivere lo stesso modello esatto, ma con RxJS. Se hai già familiarità con Rx, ti sentirai come a casa.

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

Alcune risorse utili di redux-saga

2017 consiglia

  • Non abusare della saga di Redux solo per il gusto di usarla. Solo le chiamate API testabili non ne valgono la pena.
  • Non rimuovere i thunk dal progetto per la maggior parte dei casi semplici.
  • Non esitate a inviare thunk yield put(someActionThunk)se ha senso.

Se hai paura di usare Redux-saga (o Redux-osservabile) ma hai solo bisogno del modello di disaccoppiamento, controlla redux-dispatch-iscriviti : consente di ascoltare gli invii e attivare nuovi invii nell'ascoltatore.

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

64
Questo sta migliorando ogni volta che rivisito. Valuta di trasformarlo in un post sul blog :).
RainerAtSpirit,

4
Grazie per una buona scrittura. Tuttavia non sono d'accordo su alcuni aspetti. Come è imperativo LOAD_USER? Per me, non è solo dichiarativo, ma offre anche un ottimo codice leggibile. Come ad es. "Quando premo questo pulsante voglio ADD_ITEM". Posso guardare il codice e capire esattamente cosa sta succedendo. Se invece fosse stato chiamato qualcosa per l'effetto di "BUTTON_CLICK", avrei dovuto cercarlo.
swelet,

4
Bella risposta. C'è un'altra alternativa ora: github.com/blesh/redux-observable
swennemen

4
@swelet scusa per il ritardo in risposta. Quando spedisci ADD_ITEM, è indispensabile perché invii un'azione che mira ad avere un effetto sul tuo negozio: ti aspetti che l'azione faccia qualcosa. Essere dichiarativi abbraccia la filosofia del sourcing di eventi: non invii azioni per innescare modifiche alle tue applicazioni, ma invii eventi passati per descrivere cosa è successo nella tua applicazione. L'invio di un evento dovrebbe essere sufficiente per considerare che lo stato dell'applicazione è cambiato. Il fatto che esista un negozio Redux che reagisce all'evento è un dettaglio di implementazione opzionale
Sebastien Lorber,

3
Non mi piace questa risposta perché distrae dalla domanda reale per commercializzare la propria biblioteca di qualcuno. Questa risposta fornisce un confronto tra le due librerie, che non era l'intento della domanda. La vera domanda è chiedersi se usare assolutamente il middleware, il che è spiegato dalla risposta accettata.
Abhinav Manchanda,

31

La risposta breve : mi sembra un approccio totalmente ragionevole al problema dell'asincronia. Con un paio di avvertimenti.

Avevo una linea di pensiero molto simile quando lavoravo a un nuovo progetto che avevamo appena iniziato nel mio lavoro. Sono stato un grande fan dell'elegante sistema Vanilla Redux per l'aggiornamento del negozio e la restituzione dei componenti in un modo che rimane fuori dalle viscere di un albero di componenti React. Mi è sembrato strano collegarmi a quell'elegante dispatchmeccanismo per gestire l'asincronia.

Ho finito con un approccio molto simile a quello che hai lì in una libreria che ho preso in considerazione dal nostro progetto, che abbiamo chiamato controller di reazione-redux .

Ho finito per non seguire l'approccio esatto che hai sopra per un paio di motivi:

  1. Come hai scritto, quelle funzioni di invio non hanno accesso al negozio. Puoi in qualche modo aggirare ciò facendo passare i componenti dell'interfaccia utente in tutte le informazioni necessarie alla funzione di invio. Ma direi che questo accoppia inutilmente quei componenti dell'interfaccia utente alla logica di dispacciamento. E più problematicamente, non esiste un modo ovvio per la funzione di dispacciamento di accedere allo stato aggiornato nelle continuazioni asincrone.
  2. Le funzioni di dispacciamento hanno accesso a dispatchse stesso tramite ambito lessicale. Ciò limita le opzioni per il refactoring una volta che quella connectdichiarazione sfugge di mano - e sembra piuttosto ingombrante con quel solo updatemetodo. Quindi hai bisogno di un sistema per permetterti di comporre quelle funzioni di dispatcher se le suddividi in moduli separati.

Nel complesso, è necessario allestire un sistema per consentire dispatchl'iniezione del negozio nelle funzioni di dispacciamento, insieme ai parametri dell'evento. Conosco tre approcci ragionevoli a questa iniezione di dipendenza:

  • redux-thunk lo fa in modo funzionale, passandoli nei tuoi thunk (rendendoli non esattamente thunk, per definizione di dome). Non ho lavorato con gli altri dispatchapprocci del middleware, ma presumo siano sostanzialmente gli stessi.
  • reatt-redux-controller lo fa con una procedura di routine. Come bonus, ti dà anche accesso ai "selettori", che sono le funzioni che potresti aver passato come primo argomento connect, piuttosto che dover lavorare direttamente con il negozio grezzo e normalizzato.
  • Potresti anche farlo nel modo orientato agli oggetti iniettandoli nel thiscontesto, attraverso una varietà di possibili meccanismi.

Aggiornare

Mi viene in mente che parte di questo enigma è una limitazione del reagente-redux . Il primo argomento per connectottenere un'istantanea dello stato, ma non l'invio. Il secondo argomento ottiene l'invio ma non lo stato. Nessuno dei due argomenti ottiene un thunk che si chiude sullo stato corrente, per poter vedere lo stato aggiornato al momento di una continuazione / callback.


22

L'obiettivo di Abramov - e idealmente tutti - è semplicemente quello di incapsulare la complessità (e le chiamate asincrone) nel luogo in cui è più appropriato .

Qual è il posto migliore per farlo nel flusso di dati Redux standard? Che ne dite di:

  • Riduttori ? Non c'è modo. Dovrebbero essere funzioni pure senza effetti collaterali. L'aggiornamento del negozio è un'attività seria e complicata. Non contaminarlo.
  • Componenti stupidi? Sicuramente No. Hanno una preoccupazione: presentazione e interazione con l'utente, e dovrebbero essere il più semplice possibile.
  • Componenti del contenitore? Possibile, ma non ottimale. Ha senso in quanto il contenitore è un luogo in cui incapsuliamo alcune complessità relative alla vista e interagiamo con il negozio, ma:
    • I contenitori devono essere più complessi dei componenti stupidi, ma è comunque una singola responsabilità: fornire collegamenti tra visualizzazione e stato / negozio. La tua logica asincrona è una preoccupazione completamente separata da quella.
    • Inserendolo in un contenitore, si bloccherebbe la logica asincrona in un singolo contesto, per una singola vista / rotta. Cattiva idea. Idealmente è tutto riutilizzabile e totalmente disaccoppiato.
  • S ome altro modulo di servizio? Cattiva idea: avresti bisogno di iniettare l'accesso al negozio, che è un incubo di manutenibilità / testabilità. Meglio andare con il grano di Redux e accedere al negozio solo usando le API / i modelli forniti.
  • Azioni e medaglie che le interpretano? Perchè no?! Per cominciare, è l'unica opzione importante che ci rimane. :-) Più logicamente, il sistema di azione è una logica di esecuzione disaccoppiata che puoi usare da qualsiasi luogo. Ha accesso al negozio e può inviare più azioni. Ha una sola responsabilità che è quella di organizzare il flusso di controllo e dati intorno all'applicazione e la maggior parte degli asincroni si adatta perfettamente a questo.
    • E i creatori di azioni? Perché non fare semplicemente l'asincronizzazione lì dentro, invece che nelle azioni stesse e nel Middleware?
      • Primo e, cosa più importante, i creatori non hanno accesso al negozio, come fa il middleware. Ciò significa che non puoi inviare nuove azioni contingenti, non puoi leggere dal negozio per comporre il tuo asincrono, ecc.
      • Quindi, mantieni la complessità in un luogo complesso di necessità e mantieni tutto il resto semplice. I creatori possono quindi essere funzioni semplici, relativamente pure, facili da testare.

Componenti del contenitore : perché no? A causa del ruolo svolto dai componenti in React, un container può fungere da classe di servizio e ottiene già un negozio tramite DI (oggetti di scena). Inserendolo in un contenitore, si bloccherebbe la logica asincrona in un singolo contesto, per una singola vista / rotta - come? Un componente può avere più istanze. Può essere disaccoppiato dalla presentazione, ad es. Con render prop. Immagino che la risposta potrebbe trarre ancora più vantaggio da brevi esempi che dimostrano il punto.
Estus Flask,

Adoro questa risposta!
Mauricio Avendaño,

13

Per rispondere alla domanda che viene posta all'inizio:

Perché il componente contenitore non può chiamare l'API asincrona e quindi inviare le azioni?

Tieni presente che quei documenti sono per Redux, non Redux più React. I negozi Redux collegati ai componenti di React possono fare esattamente quello che dici, ma un negozio Plain Jane Redux senza middleware non accetta argomenti a dispatcheccezione di semplici oggetti.

Ovviamente senza middleware si potrebbe ancora fare

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

Ma si tratta di un caso simile in cui l'asincronia è avvolto intorno Redux piuttosto che gestito da Redux. Pertanto, il middleware consente l'asincronia modificando ciò a cui è possibile passare direttamente dispatch.


Detto questo, lo spirito del tuo suggerimento è, credo, valido. Esistono sicuramente altri modi per gestire l'asincronia in un'applicazione Redux + React.

Uno dei vantaggi dell'utilizzo del middleware è che puoi continuare a utilizzare i creatori di azioni normalmente senza preoccuparti di come sono collegati. Ad esempio, usando redux-thunk, il codice che hai scritto sarebbe molto simile

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

che non sembra molto diverso dall'originale - è solo un po 'mischiato - e connectnon sa che updateThingè (o deve essere) asincrono.

Se anche tu volessi supportare promesse , osservabili , saghe o creatori di azioni folli e altamente dichiarativi , Redux può farlo semplicemente cambiando ciò a cui passi dispatch(ovvero ciò a cui ritorni dai creatori di azioni). Non è necessario il muck con i componenti (o le connectchiamate) di React .


Si consiglia di inviare ancora un altro evento al completamento dell'azione. Non funzionerà quando è necessario mostrare un avviso () dopo il completamento dell'azione. Le promesse all'interno dei componenti di React funzionano però. Attualmente raccomando l'approccio Promesse.
catamphetamine

8

OK, iniziamo a vedere come funziona il middleware per primo, che risponde abbastanza alla domanda, questo è il codice sorgente una funzione pplyMiddleWare in 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
      });
    };
  };
}

Guarda questa parte, guarda come la nostra spedizione diventa una funzione .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Si noti che a ciascun middleware verranno assegnate le funzioni dispatche getStatecome argomenti denominati.

OK, ecco come si presenta Redux-thunk come uno dei middleware più utilizzati per Redux:

Il middleware Redux Thunk consente di scrivere creatori di azioni che restituiscono una funzione anziché un'azione. Il thunk può essere utilizzato per ritardare l'invio di un'azione o per l'invio solo se viene soddisfatta una determinata condizione. La funzione interna riceve i metodi store store invio e getState come parametri.

Quindi, come vedi, restituirà una funzione anziché un'azione, significa che puoi aspettare e chiamarla ogni volta che vuoi in quanto è una funzione ...

Quindi che diamine è thunk? Ecco come viene introdotto in Wikipedia:

Nella programmazione del computer, un thunk è una subroutine utilizzata per iniettare un calcolo aggiuntivo in un'altra subroutine. I thunk vengono principalmente utilizzati per ritardare un calcolo fino a quando non è necessario o per inserire operazioni all'inizio o alla fine dell'altra subroutine. Hanno una varietà di altre applicazioni per la generazione del codice del compilatore e nella programmazione modulare.

Il termine nasce come derivato giocoso di "pensare".

Un thunk è una funzione che avvolge un'espressione per ritardarne la valutazione.

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

Quindi vedi quanto è semplice il concetto e come può aiutarti a gestire le tue azioni asincrone ...

È qualcosa che puoi vivere senza di essa, ma ricorda che in programmazione ci sono sempre modi migliori, più accurati e adeguati per fare le cose ...

Applica il middleware Redux


1
La prima volta su SO, non ha letto nulla. Ma mi è piaciuto solo il post che guarda l'immagine. Incredibile, suggerimento e promemoria.
Bhojendra Rauniyar,

2

Utilizzare Redux-saga è il miglior middleware nell'implementazione di React-redux.

Es .: 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;

E poi 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()
       ]
    }

E poi action.js

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

E poi riduzioneer.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; 

E poi 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'));

prova questo .. sta funzionando


3
Questa è una cosa seria per qualcuno che vuole semplicemente chiamare un endpoint API per restituire un'entità o un elenco di entità. Mi consiglia "fai questo ... poi questo, poi questo, poi quest'altra cosa, poi quello, poi quest'altra roba, poi continua, poi fai ..". Ma amico, questo è FRONTEND, dobbiamo solo chiamare il BACKEND per darci dati pronti per essere utilizzati sul frontend. Se questa è la strada da percorrere, qualcosa è sbagliato, qualcosa è davvero sbagliato e qualcuno al giorno d'oggi non applica KISS
zameb

Ciao, usa prova a catturare il blocco per le chiamate API. Una volta che l'API ha fornito la risposta, chiama i tipi di azione di Riduttore.
SM Chinna,

1
@zameb Potresti avere ragione, ma la tua lamentela riguarda lo stesso Redux e tutto ciò che viene ascoltato mentre provi a ridurre la complessità.
jorisw,

1

Ci sono creatori di azioni sincrone e poi ci sono creatori di azioni asincrone.

Un creatore di azioni sincrone è uno che quando lo chiamiamo, restituisce immediatamente un oggetto Action con tutti i dati rilevanti collegati a quell'oggetto ed è pronto per essere elaborato dai nostri riduttori.

I creatori di azioni asincrone è uno di quelli in cui richiederà un po 'di tempo prima che sia pronto a inviare un'azione.

Per definizione, ogni volta che si dispone di un creatore di azioni che effettua una richiesta di rete, si qualificherà sempre come creatore di azioni asincrono.

Se vuoi avere creatori di azioni asincrone all'interno di un'applicazione Redux devi installare qualcosa chiamato middleware che ti permetterà di gestire quei creatori di azioni asincrone.

Puoi verificarlo nel messaggio di errore che ci dice di usare il middleware personalizzato per le azioni asincrone.

Che cos'è un middleware e perché ne abbiamo bisogno per il flusso asincrono in Redux?

Nel contesto di middleware redux come redux-thunk, un middleware ci aiuta a gestire i creatori di azioni asincrone poiché è qualcosa che Redux non è in grado di gestire.

Con un middleware integrato nel ciclo Redux, stiamo ancora chiamando i creatori di azioni, che restituiranno un'azione che verrà inviata, ma ora quando inviamo un'azione, anziché inviarla direttamente a tutti i nostri riduttori, stiamo andando per dire che un'azione verrà inviata attraverso tutti i diversi middleware all'interno dell'applicazione.

All'interno di una singola app Redux, possiamo avere il numero di middleware che desideriamo. Per la maggior parte, nei progetti su cui lavoriamo avremo uno o due middleware collegati al nostro negozio Redux.

Un middleware è una semplice funzione JavaScript che verrà chiamata con ogni singola azione che inviamo. All'interno di quella funzione, un middleware ha l'opportunità di impedire che un'azione venga inviata a uno dei riduttori, può modificare un'azione o limitarsi a un'azione in qualsiasi modo tu, ad esempio, potremmo creare un middleware che consenta di registrare ogni azione che invii solo per il tuo piacere di visione.

Esiste un numero enorme di middleware open source che puoi installare come dipendenze nel tuo progetto.

Non si è limitati a fare uso del middleware open source o installarli come dipendenze. Puoi scrivere il tuo middleware personalizzato e usarlo all'interno del tuo negozio Redux.

Uno degli usi più popolari del middleware (e arrivare alla tua risposta) è per gestire i creatori di azioni asincrone, probabilmente il middleware più popolare là fuori è redux-thunk e si tratta di aiutarti a gestire i creatori di azioni asincrone.

Esistono molti altri tipi di middleware che ti aiutano anche a gestire i creatori di azioni asincrone.


1

Per rispondere alla domanda:

Perché il componente contenitore non può chiamare l'API asincrona e quindi inviare le azioni?

Direi per almeno due motivi:

Il primo motivo è la separazione delle preoccupazioni, non è il compito di action creatorrichiamare apie recuperare i dati, devi passare due argomenti al tuo action creator function, il action typee unpayload .

Il secondo motivo è perché redux storeè in attesa di un oggetto semplice con tipo di azione obbligatoria e facoltativamente apayload (ma qui è necessario passare anche il payload).

Il creatore dell'azione dovrebbe essere un oggetto semplice come di seguito:

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

E il lavoro di Redux-Thunk midlewareal dispacherisultato del tuo api callper l'appropriato action.


0

Quando si lavora in un progetto aziendale, ci sono molti requisiti disponibili in middleware come (saga) non disponibile nel flusso asincrono semplice, di seguito sono riportati alcuni:

  • Esecuzione della richiesta in parallelo
  • Tirare le azioni future senza la necessità di aspettare
  • Chiamate non bloccanti Effetto gara, primo esempio di pickup
  • risposta per avviare il processo Sequenza delle attività (prima alla prima chiamata)
  • Composizione
  • Annullamento dell'attività Avviamento dinamico dell'attività.
  • Supporto concorrenza Esecuzione di Saga al di fuori del middleware redux.
  • Utilizzando i canali

L'elenco è lungo, basta rivedere la sezione avanzata nella documentazione della saga

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.