Redux - fromScarso2King - 8 - I Middleware
Come imbrogliare Redux e farla franca
Nelle puntate precedenti abbiamo visto come gestire i dati all’interno dello stato applicativo, come aggiornare lo stato e come presentare questi dati nell’interfaccia.
Spesso e volentieri però, i dati di cui abbiamo bisogno arrivano da servizi esterni, comunemente il back-end del nostro sito internet o della nostra app.
In questi casi dobbiamo fare in modo di portare questi dati all’interno del nostro stato, così da poterli gestire esattamente come abbiamo già visto in precedenza; Per farlo abbiamo bisogno di un middleware:
Abbiamo già descritto questo flusso nella puntata 1, recuperala se non l’hai vista qui.
Come mai un middleware?
Risposta breve: Perché Redux è stupido 🤪 niente di personale.
Redux non possiede, nativamente, delle logiche per la gestione di dati asincroni; Sa solo come emettere action, aggiornare lo stato e segnalare il cambiamento alla UI in maniera sincrona, qualsiasi evento asincrono deve essere gestito esternamente allo store.
Questa scelta è motivata dalla natura dei reducer; essi sono infatti delle pure functions e se Redux permettesse la gestione di chiamate al server, per esempio, questa fisionomia andrebbe persa, in quanto una chiamata ad un servizio esterno è un side-effect.
Fatta questa premessa la domanda sporge spontanea: Come possiamo introdurre un evento asincrono senza rompere il pattern?
Beh.. possiamo imbrogliare Redux! chi l’ha detto che tutte le action devono passare per il reducer?
Come imbrogliare Redux… e farla franca
Supponiamo di avere una pagina dove faremo vedere una lista di articoli, come nella puntata precedente, però questa volta, gli articoli dovremmo recuperarli dal server:
const Articles = () => {
const articles = useSelector(getArticles); return <div>.....</div>;
}
Come facciamo a metterli all’interno dello stato così che siano reperibili dal componente Articles?
Come detto prima, le action non devono per forza passare dal reducer; nella puntata sui reducer questo viene chiarito: se non esiste un addCase relativo alla action nel builder, essa non verrà gestita.
Ecco quindi che subentrano i middleware; il loro compito sarà quello di intercettare alcune action, eseguire funzioni asincrone come ad esempio chiamate verso il server e successivamente dispatchare altre action che, invece, verranno intercettate dal reducer. In tutto questo quell’ingenuo di Redux non si renderà conto di niente 😈.
Proseguiamo, aggiorniamo il codice e facciamo in modo che se non abbiamo articoli nello stato, dispatchamo la action getArticlesAction
:
import { getArticlesAction } from 'actions';const Articles = () => {
const dispatch = useDispatch();
const articles = useSelector(getArticles); useEffect(() => {
if (!articles) {
dispatch(getArticlesAction());
}
}, [articles, dispatch]); return <div>{articles ? ... : 'Loading...'</div>;
}
Questa action non verrà gestita dal reducer, verrà invece intercettata dal middleware che effettuerà la chiamata al server, per ora omettiamo il come questa action verrà intercettata:
// Dentro il middleware
async function handleGetArticles() {
// Richiedo al server gli articoli
const response = await api.get('http://.../articles'); // Una volte ricevuti emettiamo una action con gli articoli
// appena ricevuti come payload
dispatch(getArticlesSuccess(response));
}
L’action getArticlesSuccess
invece verrà intercettata dal reducer che salverà gli articoli nello stato:
// Reducer degli articoli
function handleGetArticlesSuccess(state, action) {
return {
...state,
articles: [...action.payload],
};
}
Riprendiamo il codice e analizziamo cosa succede:
import { getArticlesAction } from 'actions';const Articles = () => {
const dispatch = useDispatch();
const articles = useSelector(getArticles); useEffect(() => {
if (!articles) {
dispatch(getArticlesAction());
}
}, [articles, dispatch]); return <div>{articles ? ... : 'Loading...'</div>;
}
Al primo render il selettore non ci restituirà nessun articolo, quindi entro nella condizione if (!articles) all’interno dello useEffect e verrà dispatchata la action getArticlesAction
. In interfaccia noto la stringa ‘Loading…’ al posto degli articoli
La action viene intercettata dal middleware che esegue la funzione asincrona handleGetArticles, essa contatta il server tramite l’API, una volta ricevuti gli articoli dispatcha la action getArticlesSuccess passando gli articoli ricevuti come payload.
Questa action viene intercettata dal reducer, lo stato viene popolato con gli articoli e causa un re-render. Al secondo render il selettore restituisce degli articoli, quindi la condizione !articles non è rispettata e vedrò gli articoli in interfaccia.
Dal punto di vista di Redux, tutto è avvenuto in maniera sincrona; Il reducer ha semplicemente ricevuto la action getArticlesSuccess con gli articoli nel payload… stupido Redux quante cose che non sai.
Guida Galattica per la scelta di un middleware
Esistono una quantità enorme di middleware, essi infatti possono essere utilizzati in moltissimi casi differenti, non solo per la gestione di action asincrone. Per capire cos’è un middleware e magari come crearne uno vostro, vi consiglio questo articolo.
Nella maggior parte dei casi però non abbiamo bisogno di creare un middleware custom, ma vogliamo semplicemente comunicare con servizi esterni e immagazzinare su Redux le informazioni ricevute, in questi casi possiamo optare per i seguenti Middleware:
Redux thunk:
Un Thunk è tipicamente un termine utilizzato per indicare un pezzo di codice che ha un comportamento asincrono, Redux Thunk implementa questo concetto adattandolo a Redux.
Un esempio di thunk è il seguente:
import { api } from '../../api'// Thunk
export async function fetchArticles(dispatch, getState) {
// Aspetto la risposta del server
const response = await api.get('/articles')
// Dispatcho la action verso il reducer con gli articoli
// ricevuti dal server
dispatch({ type: 'articles/fetch/success', payload: response })
}// All'interno di un componente:// ..
dispatch(fetchArticles);
Il middleware si occuperà di intercettare il thunk e aspettare il completamento della chiamata alla API prima di procedere verso il reducer.
Redux Saga:
Questo middleware sarà l’argomento della prossima puntata, qui la documentazione ufficiale:
A differenza di Redux Thunk, tramite Redux Saga si possono utilizzare normali action come trigger per il middleware piuttosto che thunk. Una delle particolarità di saga è l’utilizzo di generators.
Redux Observable:
Questo approccio invece sfrutta RxJS e quindi il concetto di Observable:
Per capire a fondo questa libreria è necessario avere una conoscenza di programmazione reattiva, funzionale e della libreria RxJS; per questo motivo consigliamo di optare per questa soluzione, così come per Redux Saga, solo in casi di applicativi complessi dove il semplice Redux Thunk non performerebbe altrettanto bene.
Continua la serie
Con I middleware possiamo intrometterci nel flusso che porta le action verso il reduce. Un utilizzo tipico dei middleware è quello di gestire comportamenti asincroni, nella prossima puntata vedremo Redux Saga, uno dei middleware più usati per gestire side-effects e comportamenti asincroni.
Qui sotto trovi i link alle altre puntate di questa serie
Fuori serie: 0. Le basi