Nella lezione precedente abbiamo fatto una panoramica di Redux elencandone i principi fondamentali. In questo articolo cercheremo di analizzare in dettaglio come funziona Redux. Vedremo prima quali sono gli elementi essenziali per usare Redux nelle nostre applicazioni. In Redux possiamo infatti distinguere tre componenti: Action, Reducer, Store. Vediamo allora di cosa si tratta e come sono fra loro correlati.
Primo elemento: Action
Partiamo con le Action che sono dei semplici oggetti javascript utilizzati per inviare informazioni allo Store. Si tratta dellโunico mezzo attraverso il quale si puรฒ richiedere lโaggiornamento delle informazioni presenti nello Store che รจ lโoggetto che mantiene lo stato dellโintera applicazione. Lโunico requisito delle Action รจ che devono essere un oggetto contentente una proprietร type che รจ la sola proprietร necessaria affinchรฉ un oggetto possa definirsi unโAction. Oltre alla proprietร type, possiamo aggiungere altre proprietร , a seconda delle necessitร e delle informazioni che vogliamo passare ai Reducer che sono i destinatari finali delle Action.
Vediamo come definire unโAction. Riprendiamo lโesempio con cui abbiamo terminato il precedente articolo e modifichiamolo leggermente. Ricapitolando, immaginiamo di avere una semplice applicazione in cui abbiamo un elenco di piloti. Cliccando sul nome di un pilota, visualizziamo la sua scheda dettagliata.
Per prima cosa creeremo un file actionTypes.js in cui definiamo i nomi delle diverse Action. ร opportuno definire i nomi delle diverse Action come costanti allโinterno di un file per evitare errori nella scrittura e semplificare lo sviluppo delle applicazioni. Questo accorgimento puรฒ salvare tempo soprattutto quando si realizzano applicazioni piรน complicate.
// actionTypes.js
export const CURRENT_DRIVER = 'CURRENT_DRIVER';
A questo punto definiamo le diverse Action in un file che chiameremo action.js. Potremmo creare un semplice oggetto come il seguente.
{
type: 'CURRENT_DRIVER'
}
Ma se ricordate, nellโesempio precedente avevamo aggiunto unโaltra proprietร driver che verrร usata per selezionare un preciso pilota della lista. Rispetto al precedente articolo, nel selezionare un pilota, passeremo, alla proprietร driver dellโAction, un riferimento allโintero oggetto contenente le informazioni del pilota. Avremo quindi un oggetto come quello mostrato qui sotto.
{
type: 'CURRENT_DRIVER',
driver: {name: "Nome del Pilota", team: "Scuderia del Pilota"}
}
Risulta allora evidente che รจ necessario utilizzare delle funzioni per creare le Action in maniera dinamica. Allโinterno del file action.js inseriremo quelli che Redux definisce Action creators. Si tratta di funzioni che restituiscono unโazione. Definiamo allora una funzione chooseDriver(driver).
import { CURRENT_DRIVER } from './actionTypes.js';
/*
* @param {Object} driver - il pilota selezionato all'interno della lista
* @returns {Object} - l'oggetto Action
*/
function chooseDriver(driver) {
return {
type: CURRENT_DRIVER,
driver // รจ equivalente a scrivere driver: driver
}
}
Per inviare lโoggetto Action allo Store e quindi passarlo ai Reducer invocheremo il metodo store.dispatch(). Per esempio, allโinterno della nostra applicazione chiameremo la funzione dispatch() nel seguente modo. (Abbiamo supposto che driver sia una variabile definita da qualche parte.)
// ...
store.dispatch(chooseDriver(driver));
Allโinterno del metodo store.dispatch(), lโAction, passata come argomento, verrร a sua volta passata ai Reducer.
Secondo elemento: i Reducer
I Reducer sono le uniche funzioni autorizzate ad aggiornare le informazioni contenute nello Store il quale รจ lโoggetto predisposto a mantenere lo stato dellโintera applicazione.
ร importante sottolineare che le funzioni Reducer non modificano lโoggetto State, ricevono in ingresso lโoggetto State corrente e restituiscono un nuovo oggetto State. A tal fine, nel caso si debba lavorare con oggetti o array, puรฒ essere utile usare la funzione Object.assign() e lโoperatore spread (โฆ). Di estremo interesse sono anche librerie come Immutable che perรฒ non tratteremo in questi articoli.
Dati gli argomenti in ingresso, una funzione Reducer deve calcolare il prossimo valore dellโoggetto State e restituirlo.
I Reducer, essendo funzioni pure, non devono mai modificare lโoggetto State, verrร sempre restituito un nuovo oggetto.
Una volta definito quale sarร lโoggetto State che rappresenta lo stato globale dellโapplicazione, possiamo definire la funzione Reducer che potrร aggiornarlo. Possiamo usare due approcci. Creare un Reducer e poi eventualmente riorganizzare il codice e scomporre tale Reducer in piรน funzioni. Oppure creare una funzione Reducer per ogni pezzo dellโoggetto State e poi combinare tali funzioni in unโunico Reducer.
Sulla documentazione di Redux รจ utilizzato un approccio che possiamo classificare come top-down. Viene cioรจ definito un generico Reducer che viene poi scomposto in piรน reducer ciascuno dei quali gestisce una parte dello stato complessivo. Al fine di spiegare come funzionano i Reducer, puรฒ risultare perรฒ piรน semplice usare una tecnica bottom-up. Potete comunque consultare la documentazione ufficiale per ulteriori spiegazioni. Consideriamo intanto come sarร la struttura dellโoggetto State della nostra applicazione.
{
currentDriver: {name: "Lewis Hamilton", team: "Mercedes"},
drivers: [
{
name: "Sebastian Vettel",
team: "Ferrari"
},
{
name: "Lewis Hamilton",
team: "Mercedes"
},
{
name: "Max Verstappen",
team: "Red Bull Racing"
}
]
}
Abbiamo detto nel precedente articolo che รจ possibile combinare piรน Reducer insieme. Allora, per ogni proprietร dellโoggetto State che vogliamo aggiornare, definiamo una funzione Reducer che avrร lโincarico di gestire solo quella singola parte dellโintero oggetto State. In questo particolare caso, creiamo un Reducer per ogni proprietร , per mostrare come sia possibile combinarli insieme per creare un unico Reducer. ร comunque facoltร del programmatore capire e decidere come e se comporre o scomporre i diversi Reducer. Alla fine ciรฒ che dovrร essere creato รจ un singolo Reducer (chiameremo questo Reducer col nome rootReducer) che si occuperร di aggiornare lโintero oggetto State dellโapplicazione e che verrร passato alla funzione createStore(rootReducer) per creare lo Store. (Ne parleremo a breve)
Definiamo allora le seguenti funzioni.
function selectDriver(lastDriverSelected = {}, action) {
if (action.type === 'CURRENT_DRIVER') {
return action.driver;
}
if (
action.type === 'REMOVE_DRIVER' &&
action.driver.name === lastDriveSelected.name
) {
return {};
}
return lastDriverSelected;
}
function drivers(previousListOfDrivers = [], action) {
switch(action.type) {
case 'ADD_DRIVER':
return [
...previousListOfDrivers,
{
name: action.driver.name,
team: action.driver.team
}
]
case 'REMOVE_DRIVER':
return previousListOfDrivers.filter(driver =>
driver.name != action.driver.name
)
default:
return previousListOfDrivers;
}
}
Quando il Reducer drivers() riceve un Action con proprietร type uguale a โADD_DRIVERโ, usiamo lโoperatore spread (โฆ) per restituire un nuovo array, composto da tutti i valori presenti nel vecchio array a cui abbiamo concatenato il nuovo elemento da aggiungere. (ร equivalente a scrivere previousListOfDrivers.concat(newDriver) dove newDriver รจ un oggetto avente come proprietร name e team).
A questo punto abbiamo due funzioni ognuna delle quali gestisce un pezzo dellโoggetto State della nostra applicazione. Dobbiamo creare un Reducer che gestisca lโintero oggetto State. Possiamo farlo combinando le due funzioni appena create. Possiamo usare la funzione combineReducers, definita nel modulo redux che possiamo scaricare con NPM. La funzione combineReducers riceve come argomento un oggetto in cui ogni proprietร ha come valore un riferimento a una funzione Reducer e restituisce una funzione Reducer che a sua volta ha come valore di ritorno un altro oggetto. Questโultimo avrร delle proprietร che hanno lo stesso nome di quelle dellโoggetto passato come argomento a combineReducers e per valore il risultato dellโesecuzione di quelle funzioni. Lโoggetto restituito dal Reducer complessivo costituirร lโoggetto State dellโapplicazione.
Vediamo un esempio per comprendere meglio il comportamento della funzione combineReducers.
import { combineReducers } from 'redux'
//... codice delle funzioni definite sopra
const rootReducer = combineReducers({
currentDriver: selectDriver,
drivers
});
Come abbiamo detto, combineReducers restituisce una funzione equivalente alla seguente.
export default function rootReducer(state = {}, action) {
return {
currentDriver: selectDriver(state.currentDriver, action),
drivers: drivers(state.drivers, action)
}
}
Notate che lโoggetto restituito dal Reducer "complessivo" ha come proprietร le stesse proprietร dellโoggetto passato come argomento alla funzione combineReducers che sono le stesse dellโoggetto State dellโapplicazione. Nel caso della funzione rootReducer(), se lโoggetto state iniziale, passato come primo argomento รจ vuoto, le funzioni selectDriver e drivers riceveranno come primo argomento un oggetto che รจ undefined. Di conseguenza, useranno il valore di default ovvero {} e [] rispettivamente.
Terzo elemento: lo Store
Lโultimo elemento essenziale di Redux รจ lo Store che รจ lโoggetto che mantiene lโoggetto State dellโapplicazione e fornisce alcuni metodi come getState() che restituisce lโoggetto State dellโapplicazione e dispatch(action) che serve per lanciare unโAction. Store ha inoltre una funzione subscribe(listener) che serve per registrare una funzione che verrร invocata ogni volta che unโAction verrร lanciata da dispatch(action). Allโinterno della funzione che passiamo al metodo subscribe(), possiamo leggere il valore attuale dellโoggetto State invocando getState(). La funzione subscribe() รจ la funzione che useremo nellโapplicazione per essere notificati che unโAction รจ stata lanciata e che รจ possibile che sia disponibile una versione aggiornata dellโoggetto State. Vedremo nel prossimo articolo come usare la funzione subscribe() per fare in modo che i componenti della nostra applicazione React vengano aggiornati in seguito alla modifica dellโoggetto State.
Per creare lo Store useremo la funzione createStore(rootReducer) fornita da Redux. La funzione createStore riceve altri argomenti che per ora trascuriamo. Il primo argomento da passare a createStore รจ il Reducer "complessivo" che restituisce lโoggetto State aggiornato. Nel nostro caso dovremo passare rootReducer.
import { createStore } from 'redux'
import rootReducer from './reducers'
const store = createStore(rootReducer);
Come spiegato dal creatore di Redux, Dan Abramov, la funzione createStore รจ essenzialmente la seguente. (Per maggiori dettagli potete leggere il codice di createStore nella repository GitHub di Redux)
const createStore = (rootReducer) => {
let state;
let listeners = [];
const dispatch = (action) => {
state = rootReducer(state, action);
listeners.forEach(listener => listener());
}
const getState = () => state;
const subscribe = (newListener) => {
listeners.push(newListener);
return () => {
listeners = listeners.filter(listener => listener !== newListener)
}
}
dispatch({});
return { dispatch, getState, subscribe };
}
Notate che la funzione subscribe() restituisce una funzione (che chiameremo unsubscribe()) che, se invocata, permette di rimuovere newListener dallโarray delle funzioni che verranno invocate ogni volta che viene eseguita la funzione dispatch(). Viene inoltre invocata (subito prima del return) la funzione dispatch() passando unโAction che รจ un oggetto vuoto. In questo modo viene inizializzato lโoggetto State con il valore iniziale restituito dalla chiamata alla funzione rootReducer(undefined, {}).
Come funziona unโapplicazione che usa Redux
Per concludere, riassumiamo con unโimmagine come funziona unโapplicazione che usa Redux. Usiamo lโesempio descritto sopra in cui abbiamo una lista di piloti e possiamo selezionare uno dei piloti per mostrare la sua scheda dettagliata. Se lโimmagine risulta di difficile lettura, potete trovare la versione in alta risoluzione su Dropbox
Nel prossimo articolo vedremo come collegare Redux e React attraverso lโuso di react-redux.