Redux - fromScarso2King - 9 - Le Saga
yield select(getSubTitle)
Premessa
Redux Saga è una libreria vastissima e complessa, in particolar modo per chi non ha dimestichezza con i generators, prima di procedere quindi vi consiglio di leggere qualcosa sui generatori, in particolare questo articolo; un altro articolo che consiglio è questo, dove viene chiarita la correlazione tra async await e generators.
Redux Saga
Saga è uno tra i middleware più amati dalla comunità Redux, i motivi della sua popolarità sono molteplici, elenco qui le ragioni per cui noi abbiamo optato questo middleware come elemento fondamentale dei nostri progetti:
- è facile da testare
- è scalabile
- Rende molto facile la gestione di processi complessi tramite le proprie utility
- Permette di scrivere codice molto leggibile
- Ha un nome buffo perché ricorda Candy Crush Saga
Pur essendo così popolare però, necessità di un pò di pratica prima di essere compreso a pieno, cercherò di introdurla attraverso i suoi concetti chiave e qualche esempio.
Per utilizzare Saga all’interno del nostro sito / app, come ogni middleware, dobbiamo registralo nello store, per farlo bisogna utilizzare gli helper applyMiddleware di Redux e createSagaMiddleware, esposto invece da redux-saga:
Se invece utilizziamo redux-toolkit, al posto di createStore avremo:
export const store = configureStore({
reducer,
middleware: [sagaMiddleware]
});
Tutto molto bello ma… che cosa è una saga?
Possiamo vedere la saga come una combo watcher — worker, il watcher resta in ascolto di una (o più) action, il worker viene richiamato dal watcher a seguito di una action ricevuta. Premetto che questa definizione non è ufficiale (sia watcher che worker sono delle saga), ma rende molto bene il concetto.
Facciamo un esempio:
In questo snippet ci sono sicuramente alcune funzioni che, per chi non ha mai visto saga, possono risultare strane; vorrei quindi fare un esercizio, leggiamo il codice e cerchiamo di capire che succede:
il watcher, in basso, utilizza una funzione chiamata takeEvery a cui vengono passati 2 parametri: una action ed un altra funzione (il worker). Intuitivamente quello che succede è che ogni qual volta (take every) la action indicata come primo parametro viene dispatchata esegue la funzione al secondo parametro.
Il worker, in alto, richiama una funzione “call” a cui passa l’api che utilizziamo per contattare il server; da questa funzione riceve gli articoli e se tutto è andato bene richiama un’altra funzione, put, a cui passa la action di success e gli articoli ricevuti.
Sostituiamo mentalmente la funzione put e chiamiamola dispatch, è più chiaro ora che succede?
→ action → (watcher → worker) → reducer → store → UI
Se tutto va a buon fine la saga dispatcha la action di success, che andrà nel reducer, abbiamo appena imbrogliato Redux!
Le funzioni call, put e takeEvery viste nell’esempio sono degli helper, nel dettaglio:
takeEvery → Rimane in ascolto di una action, permette di eseguire più volte il worker anche se è in esecuzione una chiamata precedente
call → è un effetto, prende come primo parametro la funzione da richiamare, gli altri parametri sono i parametri da passare alla funzione, una volta eseguita la funziona ritorna al chiamante il suo valore di ritorno
put → anch’esso è un effetto e permette di dispatchare delle action
Redux Saga però offre tantissimi altri effetti / helper, i più importanti dei quali sono sicuramente:
takeLatest → che, in contrapposizione con takeEvery, ogni volta che intercetta una nuova action, termina le esecuzioni precedente del worker e lo ri-esegue con la nuova action
take → che è la base per l’ascolto delle action, takeEvery/Latest sono costruite a partire da questa
select → che permette di leggere lo stato applicativo (vedila come un useSelector su saga)
La lista potrebbe continuare per pagine, ma questi sono i principali helper che permettono di fare quasi tutto, sicuramente tutte le operazioni più comuni.
Per capire come possiamo utilizzare queste funzionalità, facciamo qualche esempio con alcuni casi reali che spesso ci capita di gestire nelle nostre applicazioni:
Utilizzare la risposta di una chiamata per generare il payload di una action
In questo esempio vogliamo recuperare le informazioni dell’autore a partire dall’id di un articolo, quindi tramite l’id recupereremo l’articolo, da cui otterremo l’authorId che utilizzeremo per recuperare le informazioni dell’autore:
Molto facile, ma possiamo fare di più! Probabilmente abbiamo già una saga che si occupa di ottenere l’articolo a partire dall’id, in ottica di non ripetizione del codice (DRY) possiamo re-factorizzare come nell’esempio successivo.
Utilizzare un’altra saga per ottenere il payload di una nuova action
Supponiamo esista una saga che si occupa di recuperare un articolo a partire dal suo ID, come possiamo sfruttarla per non riscrivere quella logica? ricordiamo che la saga viene eseguita quando la action per cui sta in ascolto viene dispatchata, ma noi abbiamo già visto che tramite l’effetto put possiamo dispatchare action dalla nostra saga:
In questo esempio utilizziamo l’effetto take, a cui passiamo la action che vogliamo “aspettare” prima di procedere; Infatti dispatchamo la action fetchArticle, dopo di che aspettiamo che la saga vada in success e quindi che dispatchi la action fetchArticleSuccess, utilizziamo quest’ultima action per prelevare l’authorId e procedere con l’esempio. Ma possiamo fare di più.
Se la saga di fetch dell’articolo andasse in errore? come possiamo gestire anche questo caso? Beh… Sappiamo che la fetch dell’articolo può concludersi in 2 modi, success o failure, se termina in success verrà dispatchata l’action fetchArticleSuccess altrimenti verrà dispatchata fetchArticleError; Allora ci basta ascoltare entrambe e la prima che intercettiamo ci dirà come proseguire.
Gare mozzafiato tra effetti:
Esiste un effetto chiamato race che fa a caso nostro, questo effetto viene utilizzato quando vengono lanciate vari task in parallelo, ma non vogliamo aspettarli tutti, il primo che viene risolto ci basta per proseguire.
Quindi, tornando al nostro esempio, se facciamo una gara tra il success ed il failure, il primo che viene intercettato ci dirà se portare avanti la nostra saga o terminare in un failure:
Non sempre però abbiamo la necessità di richiamare servizi esterni per ottenere i dati di cui abbiamo bisogno, molto spesso sono già presenti nello stato. Saga ha pensato anche a questo, possiamo infatti riutilizzare i selector all’interno delle saga tramite l’effetto select.
Usare Selector all’interno di una Saga
In quest’ultimo esempio, invece che andare a richiamare un altra saga per recuperare l’articolo, supporremo di averlo già fetchato in precedenza, allora possiamo andare a recuperarlo direttamente dallo stato:
è facile immaginare di combinare questi effetti in una miriade di modi differenti, questa versatilità forse è il vero motivo del successo di saga, come già detto in precedenza questi che abbiamo visto sono solo una piccola parte degli effetti / helper messi a disposizioni da saga; Vi invito a leggere la documentazione dove ci sono tantissimi altri esempi e casi d’uso avanzati.
La root saga
Per finire, abbiamo visto come installare il middleware redux-saga e come creare delle saga, ma ci manca un ultimo step per vedere il meccanismo funzionare: la root saga.
Possiamo vedere la root saga come l’insieme di tutti i nostri watcher, un unico punto dove vengono eseguiti i nostri watcher che, a loro volta, eseguiranno i worker in seguito alle action intercettate.
Anche in questo caso, esistono vari modi per creare la root saga, tant’è che è stata creata un sezione apposita all’interno della documentazione:
Vi invito fortemente a leggere l’articolo, è molto breve ed elenca i vantaggi dei vari pattern, qui invece mostro un modo molto semplice che va benissimo per la maggior parte dei casi d’uso e che sfrutta l’effetto all:
L’effetto all permette di lanciare più task in parallelo, esattamente come il race, ma a differenza del race attende che terminino tutti. Per chi è più pratico con le Promises è molto simile a Promise.all.
Continua la serie
Con Redux Saga possiamo gestire l’asincronicità all’interno del nostro stato Redux, tramite questo pezzo del puzzle siamo pronti per fare la nostra Mini App dove copriremo tutti i punti toccati con il corso. Per la mini app abbiamo pensato a qualcosa che fosse istruttivo ma anche utile, in particolare se hai più di 60 anni e non vedi proprio l’ora di fare un figurone nel gruppo WhatsApp con i parenti, ma non dico altro 🙊
Qui sotto trovi i link alle altre puntate di questa serie
Fuori serie: 0. Le basi