Pro / contro dell'utilizzo di redux-saga con generatori ES6 vs redux-thunk con ES2017 async / waitit


488

Si parla molto dell'ultimo ragazzo in redux town in questo momento, redux-saga / redux-saga . Utilizza le funzioni del generatore per ascoltare / inviare azioni.

Prima di avvolgerci la testa, vorrei conoscere i pro / contro dell'utilizzo redux-sagaanziché l'approccio di seguito in cui sto usando redux-thunkcon asincrono / wait.

Un componente potrebbe apparire così, inviare azioni come al solito.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Quindi le mie azioni assomigliano a questo:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

6
Vedi anche la mia risposta confrontando redux-thunk e redux-saga qui: stackoverflow.com/a/34623840/82609
Sebastien Lorber

22
Qual è il ::prima che this.onClickfai?
Downhillski,

37
@ZhenyangHua è una scorciatoia per associare la funzione all'oggetto ( this), aka this.onClick = this.onClick.bind(this). Si consiglia di eseguire la forma più lunga nel costruttore, poiché la mano breve si ricollega su ogni rendering.
hampusohlsson,

7
Vedo. Grazie! Vedo persone che usano bind()molto per passare thisalla funzione, ma ho iniziato a usarlo () => method()ora.
Downhillski,

2
@Hosar Ho usato redux e redux-saga in produzione per un po ', ma in realtà sono migrato su MobX dopo un paio di mesi perché meno sovraccarico
hampusohlsson

Risposte:


461

In redux-saga, l'equivalente dell'esempio sopra sarebbe

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

La prima cosa da notare è che stiamo chiamando le funzioni API utilizzando il modulo yield call(func, ...args). callnon esegue l'effetto, crea semplicemente un oggetto semplice come {type: 'CALL', func, args}. L'esecuzione è delegata al middleware redux-saga che si occupa di eseguire la funzione e riprendere il generatore con il suo risultato.

Il vantaggio principale è che puoi testare il generatore al di fuori di Redux usando semplici controlli di uguaglianza

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Nota che stiamo deridendo il risultato della chiamata API semplicemente iniettando i dati derisi nel nextmetodo dell'iteratore. I dati beffardi sono molto più semplici delle funzioni beffardo.

La seconda cosa da notare è la chiamata a yield take(ACTION). I thunk vengono chiamati dal creatore dell'azione ad ogni nuova azione (ad es LOGIN_REQUEST.). cioè azioni sono continuamente spinti a thunk e thunks hanno alcun controllo su quando interrompere manipolazione di tali azioni.

In redux-saga, i generatori eseguono l'azione successiva. cioè hanno il controllo su quando ascoltare qualche azione e quando no. Nell'esempio sopra le istruzioni di flusso sono collocate all'interno di un while(true)loop, quindi ascolterà ogni azione in arrivo, il che imita in qualche modo il comportamento di spinta del thunk.

L'approccio pull consente l'implementazione di flussi di controllo complessi. Supponiamo ad esempio di voler aggiungere i seguenti requisiti

  • Gestire l'azione utente di LOGOUT

  • al primo accesso riuscito, il server restituisce un token che scade con un certo ritardo memorizzato in un expires_incampo. Dovremo aggiornare l'autorizzazione in background ogni expires_inmillisecondi

  • Tenere presente che durante l'attesa del risultato delle chiamate API (accesso iniziale o aggiornamento) l'utente può disconnettersi nel mezzo.

Come lo implementeresti con i thunk? fornendo allo stesso tempo una copertura completa dei test per l'intero flusso? Ecco come potrebbe apparire con Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

Nell'esempio sopra, stiamo esprimendo il nostro requisito di concorrenza utilizzando race. Se take(LOGOUT)vince la gara (ovvero l'utente ha fatto clic su un pulsante di logout). La gara annullerà automaticamente l' authAndRefreshTokenOnExpiryattività in background. E se è authAndRefreshTokenOnExpirystato bloccato nel mezzo di una call(authorize, {token})chiamata, verrà annullato. La cancellazione si propaga automaticamente verso il basso.

È possibile trovare una demo eseguibile del flusso sopra


@yassine da dove viene la delayfunzione? Ah, l'ho trovato: github.com/yelouafi/redux-saga/blob/…
philk

122
Il redux-thunkcodice è abbastanza leggibile e autoesplicato. Ma redux-sagasuno è veramente illeggibile, soprattutto a causa di coloro verbo-come funzioni: call, fork, take, put...
syg

11
@syg, sono d'accordo che call, fork, take e put possono essere più semanticamente amichevoli. Tuttavia, sono quelle funzioni simili a verbi che rendono testabili tutti gli effetti collaterali.
Downhillski,

3
@syg è ancora una funzione con quelle strane funzioni di verbi più leggibili di una funzione con profonde catene di promesse
Yasser Sinjab

3
quei verbi "strani" ti aiutano anche a concettualizzare la relazione della saga con i messaggi che escono dal redux. si può prendere i tipi di messaggi di redux - spesso per far scattare l'iterazione successiva, e si può mettere i nuovi messaggi di nuovo a trasmettere il risultato del vostro effetto collaterale.
worc,

104

Aggiungerò la mia esperienza con saga nel sistema di produzione oltre alla risposta piuttosto approfondita dell'autore della biblioteca.

Pro (usando saga):

  • Testabilità. È molto facile testare le saghe poiché call () restituisce un oggetto puro. Per testare i thunk normalmente è necessario includere un mockStore all'interno del test.

  • redux-saga include molte utili funzioni di supporto per le attività. Mi sembra che il concetto di saga sia quello di creare una sorta di lavoratore / thread in background per la tua app, che funga da pezzo mancante nell'architettura di reazione redux (actionCreators e riduttori devono essere funzioni pure). Il che porta al punto successivo.

  • Le saghe offrono un posto indipendente per gestire tutti gli effetti collaterali. Di solito è più facile da modificare e gestire delle azioni thunk nella mia esperienza.

con:

  • Sintassi del generatore.

  • Molti concetti da imparare.

  • Stabilità API. Sembra che redux-saga stia ancora aggiungendo funzionalità (ad es. Canali?) E la community non è così grande. C'è un problema se un giorno la libreria effettua un aggiornamento non compatibile con le versioni precedenti.


9
Voglio solo fare un commento, il creatore dell'azione non deve essere pura funzione, come è stato affermato dallo stesso Dan molte volte.
Marson Mao,

14
A partire da ora, le sagome redux sono molto raccomandate man mano che l'utilizzo e la comunità si espandono. Inoltre, l'API è diventata più matura. Prendi in considerazione la rimozione della Con API stabilitycome aggiornamento per riflettere la situazione attuale.
Denialos,

1
la saga ha più punti di partenza di thunk e anche il suo ultimo commit è dopo thunk
rinnovo il

2
Sì, la redux-saga di FWIW ora ha 12k stelle, il redux-thunk ne ha 8k
Brian Burns

3
Aggiungerò un'altra sfida delle saghe, è che le saghe sono completamente disaccoppiate da azioni e creatori di azioni per impostazione predefinita. Mentre i Thunks collegano direttamente i creatori di azioni con i loro effetti collaterali, le saghe lasciano i creatori di azioni totalmente separati dalle saghe che li ascoltano. Ciò presenta vantaggi tecnici, ma può rendere il codice molto più difficile da seguire e può offuscare alcuni dei concetti unidirezionali.
theaceofthespade il

33

Vorrei solo aggiungere alcuni commenti della mia esperienza personale (usando sia saghe che thunk):

Le saghe sono fantastiche da testare:

  • Non è necessario deridere le funzioni racchiuse in effetti
  • Pertanto i test sono puliti, leggibili e facili da scrivere
  • Quando usano le saghe, i creatori di azioni restituiscono per lo più letterali semplici oggetti. È anche più facile testare e affermare diversamente dalle promesse di Thunk.

Le saghe sono più potenti. Tutto quello che puoi fare nel creatore di azioni di un thunk puoi anche farlo in una saga, ma non viceversa (o almeno non facilmente). Per esempio:

  • attendere la spedizione di un'azione / azioni ( take)
  • cancel ordinaria esistente ( cancel, takeLatest, race)
  • più routine possono ascoltare la stessa azione ( take, takeEvery, ...)

Sagas offre anche altre utili funzionalità, che generalizzano alcuni schemi di applicazione comuni:

  • channels ascoltare su fonti esterne di eventi (ad es. websocket)
  • modello a forcella ( fork, spawn)
  • valvola a farfalla
  • ...

Le saghe sono uno strumento eccezionale e potente. Tuttavia con il potere deriva la responsabilità. Quando la tua applicazione cresce, puoi facilmente perderti capendo chi è in attesa dell'azione o cosa succede quando viene inviata un'azione. D'altra parte il thunk è più semplice e più facile da ragionare. La scelta dell'uno o dell'altro dipende da molti aspetti come il tipo e le dimensioni del progetto, i tipi di effetti collaterali che il progetto deve gestire o le preferenze del team di sviluppo. In ogni caso basta mantenere l'applicazione semplice e prevedibile.


8

Solo qualche esperienza personale:

  1. Per stile di codifica e leggibilità, uno dei vantaggi più significativi dell'utilizzo di redux-saga in passato è quello di evitare l'inferno di callback in redux-thunk: non è più necessario utilizzare molti annidamenti / catture. Ma ora con la popolarità di async / attende il thunk, si potrebbe anche scrivere codice asincrono in stile sync quando si utilizza redux-thunk, che può essere considerato un miglioramento del redux-think.

  2. Uno potrebbe aver bisogno di scrivere molto più codice boilerplate quando si usa redux-saga, specialmente in Typescript. Ad esempio, se si desidera implementare una funzione di recupero asincrono, la gestione dei dati e degli errori potrebbe essere eseguita direttamente in un'unità thunk in action.js con una singola azione FETCH. Ma in redux-saga, potrebbe essere necessario definire le azioni FETCH_START, FETCH_SUCCESS e FETCH_FAILURE e tutti i relativi controlli del tipo, perché una delle caratteristiche di redux-saga è utilizzare questo tipo di meccanismo "token" per creare effetti e istruire negozio redux per test facili. Naturalmente si potrebbe scrivere una saga senza usare queste azioni, ma ciò la renderebbe simile a un thunk.

  3. In termini di struttura dei file, redux-saga sembra essere più esplicito in molti casi. Si potrebbe facilmente trovare un codice asincrono in ogni sagas.ts, ma in redux-thunk, si dovrebbe vederlo in azioni.

  4. I test facili possono essere un'altra caratteristica ponderata in redux-saga. Questo è veramente conveniente. Ma una cosa che deve essere chiarita è che il test "call" redux-saga non eseguirà l'effettiva chiamata API nei test, quindi bisognerebbe specificare il risultato del campione per i passaggi che potrebbero usarlo dopo la chiamata API. Pertanto, prima di scrivere in redux-saga, sarebbe meglio pianificare una saga e le corrispondenti sagas.spec.ts in dettaglio.

  5. Redux-saga offre anche molte funzionalità avanzate come l'esecuzione di attività in parallelo, aiutanti di concorrenza come takeLatest / takeEvery, fork / spawn, che sono molto più potenti dei thunk.

In conclusione, personalmente, vorrei dire: in molti casi normali e app di piccole e medie dimensioni, vai con stile asincrono / attendi redux-thunk. Ti farebbe risparmiare molti codici / azioni / typedefs di plateplate, e non avresti bisogno di cambiare molte saghe.ts diverse e mantenere un albero di saghe specifico. Ma se stai sviluppando un'app di grandi dimensioni con una logica asincrona molto complessa e la necessità di funzionalità come la concorrenza / modello parallelo, o hai una forte domanda di test e manutenzione (specialmente nello sviluppo guidato dai test), le sagome redux potrebbero salvarti la vita .

Comunque, la redux-saga non è più difficile e complessa della redux stessa, e non ha una cosiddetta curva di apprendimento ripida perché ha concetti e API fondamentali ben limitati. Trascorrere un po 'di tempo nell'apprendimento della redux-saga potrebbe essere utile per te un giorno in futuro.


5

Avendo esaminato alcuni diversi progetti React / Redux su larga scala nella mia esperienza, Sagas offre agli sviluppatori un modo più strutturato di scrivere codice che è molto più facile da testare e più difficile da sbagliare.

Sì, è un po 'strano iniziare, ma la maggior parte degli sviluppatori ne ha abbastanza comprensione in un giorno. Dico sempre alla gente di non preoccuparsi di cosa yieldcominciare e che una volta che avrai scritto un paio di test ti verrà in mente.

Ho visto un paio di progetti in cui i thunk sono stati trattati come se fossero controller dal patten MVC e questo diventa rapidamente un pasticcio non stampabile.

Il mio consiglio è di usare Sagas dove hai bisogno che A faccia scattare cose di tipo B relative a un singolo evento. Per tutto ciò che potrebbe incidere su una serie di azioni, trovo che sia più semplice scrivere il middleware del cliente e utilizzare la meta proprietà di un'azione FSA per attivarla.


2

Thunks contro Sagas

Redux-Thunke Redux-Sagadifferiscono in alcuni modi importanti, entrambe sono librerie di middleware per Redux (il middleware Redux è un codice che intercetta le azioni che entrano nel negozio tramite il metodo dispatch ()).

Un'azione può essere letteralmente qualsiasi cosa, ma se stai seguendo le migliori pratiche, un'azione è un semplice oggetto javascript con un campo tipo e campi payload, meta ed errori opzionali. per esempio

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Oltre a inviare azioni standard, il Redux-Thunkmiddleware consente di inviare funzioni speciali, chiamate thunks.

I thunk (in Redux) hanno generalmente la seguente struttura:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Cioè, a thunkè una funzione che (facoltativamente) accetta alcuni parametri e restituisce un'altra funzione. La funzione interna accetta una dispatch functione una getStatefunzione, entrambe fornite dal Redux-Thunkmiddleware.

Redux-Saga

Redux-Sagail middleware consente di esprimere complesse logiche applicative come pure funzioni chiamate saghe. Le funzioni pure sono desiderabili dal punto di vista del test perché sono prevedibili e ripetibili, il che le rende relativamente facili da testare.

Le saghe sono implementate attraverso funzioni speciali chiamate funzioni del generatore. Queste sono una nuova funzionalità di ES6 JavaScript. Fondamentalmente, l'esecuzione salta dentro e fuori da un generatore ovunque vedi una dichiarazione di rendimento. Pensa a yieldun'istruzione come a far sospendere il generatore e restituire il valore prodotto. In seguito, il chiamante può riprendere il generatore all'istruzione che segue il yield.

Una funzione del generatore è definita in questo modo. Notare l'asterisco dopo la parola chiave funzione.

function* mySaga() {
    // ...
}

Una volta registrata la saga di accesso Redux-Saga. Ma poi il yieldtake sulla prima riga metterà in pausa la saga fino a quando un'azione con type non 'LOGIN_REQUEST'viene spedita al negozio. Una volta che ciò accade, l'esecuzione continuerà.

Per maggiori dettagli vedi questo articolo .


1

Una breve nota. I generatori sono cancellabili, asincroni / attendono - non. Quindi, per un esempio della domanda, non ha davvero senso cosa scegliere. Ma per flussi più complicati a volte non esiste soluzione migliore dell'utilizzo di generatori.

Quindi, un'altra idea potrebbe essere quella di utilizzare i generatori con redux-thunk, ma per me, sembra che stia cercando di inventare una bicicletta con ruote quadrate.

E, naturalmente, i generatori sono più facili da testare.


0

Ecco un progetto che combina le parti migliori (pro) di entrambi redux-sagae redux-thunk: è possibile gestire tutti gli effetti collaterali sulle saghe ottenendo una promessa dall'azione dispatchingcorrispondente: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

1
l'utilizzo then()all'interno di un componente React è contro il paradigma. È necessario gestire lo stato modificato componentDidUpdateanziché attendere la risoluzione di una promessa.

3
@ Maxincredible52 Non è vero per il rendering lato server.
Diego Haz,

Nella mia esperienza, il punto di Max è ancora vero per il rendering lato server. Questo dovrebbe probabilmente essere gestito da qualche parte nel livello di routing.
ThinkingInBits

3
@ Maxincredible52 perché è contro il paradigma, dove l'hai letto? Di solito faccio in modo simile a @Diego Haz ma lo faccio in componentDidMount (secondo i documenti di React, le chiamate di rete dovrebbero preferibilmente essere fatte lì) quindi abbiamocomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Un modo più semplice è usare redux-auto .

dalla documantasion

redux-auto ha risolto questo problema asincrono semplicemente consentendo di creare una funzione "azione" che restituisce una promessa. Per accompagnare la logica di azione della funzione "predefinita".

  1. Non è necessario altro middleware asincrono Redux. per esempio thunk, promessa-middleware, saga
  2. Ti consente facilmente di trasformare una promessa in redux e gestirla per te
  3. Consente di individuare le chiamate di servizio esterne con cui verranno trasformate
  4. La denominazione del file "init.js" lo chiamerà una volta all'avvio dell'app. Questo è utile per caricare i dati dal server all'avvio

L'idea è di avere ogni azione in un file specifico . localizzare la chiamata del server nel file con le funzioni di riduzione per "in sospeso", "soddisfatta" e "rifiutata". Questo rende molto semplice la gestione delle promesse.

Inoltre, associa automaticamente un oggetto helper (chiamato "asincrono") al prototipo del tuo stato, permettendoti di tracciare nella tua UI le transizioni richieste.


2
Ho fatto +1 anche se è una risposta irrilevante perché dovrebbero essere prese in considerazione anche soluzioni diverse
rinnovo il

12
Penso che ci siano perché non ha rivelato di essere l'autore del progetto
jreptak,
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.