Non
Ma ... dovresti usare redux-saga :)
La risposta di Dan Abramov è giusta, redux-thunk
ma 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 takeLatest
permessi 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 takeEvery
se non volessi questo comportamento.
Mantieni i creatori di azione puri. Nota che è ancora utile mantenere actionCreator (in saghe put
e 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_SCROLLED
può 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 select
effetto 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:
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.
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-saga
avrebbe 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 epic
per 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' });
}
});