back to top

Guida Redux: come funziona e quali sono gli elementi essenziali

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.

Pubblicitร 

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.

wireframe applicazione f1 react redux

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

schema funzionamento applicazione redux

Nel prossimo articolo vedremo come collegare Redux e React attraverso lโ€™uso di react-redux.

Pubblicitร