Una volta definite le azioni che determinano la variazione dello stato della nostra applicazione, vediamo come sfruttarle.
Nel presentare gli attori coinvolti nell'architettura di Redux, abbiamo detto che il ruolo del componente che permette di sotituire lo stato corrente con un altro è affidato al reducer.
Lo stato
Questo componente non è altro che una funzione che, preso uno stato ed un'azione, restituisce in maniera deterministica lo stato successivo. Ma cos'è esattamente lo stato della nostra applicazione? Ancora una volta non abbiamo vincoli su come definire lo stato: esso può essere un oggetto con qualsiasi struttura. Ad esempio, nel nostro caso abbiamo deciso di descrivere lo stato della nostra applicazione come un oggetto che ha ha una proprietà di tipo array contenente l'elenco delle attività. Il seguente esempio è uno dei possibili stati della nostra applicazione:
{
todoList: [
{id: 1, task: "Definire le azioni" },
{id: 2, task: "Definire i reducer" },
{id: 3, task: "Creare lo store" },
{id: 4, task: "Inviare le azioni allo store" }
]
}
Con questa struttura in mente, proviamo ad immaginare cosa determina su di essa l'aggiunta di una nuova attività e la rimozione di un'attività esistente sullo stato. Il compito del nostro reducer sarà appunto questo: restituire il risultato ottenuto aggiungendo o rimuovendo un' attività alla struttura dati esistente.
Tuttavia, prima di partire con la definizione del reducer dobbiamo definire qual è lo stato di partenza della nostra applicazione. Con molta probabilità esso rappresenterà un elenco di attività vuoto, che quindi possiamo rappresentare con la seguente definizione:
const initialState = { todoList: []};
Il reducer
A questo punto possiamo definire il reducer come mostrato di seguito:
function todo(state = initialState, action) {
switch(action.type) {
case ADD:
state = Object.assign({}, state, {
todoList: [ ...state.todoList,
{id: state.todoList.length + 1, task: action.payload.task }
]});
break;
case REMOVE:
state = Object.assign({}, state, {
todoList: state.todoList.filter((x) => x.id != action.payload.taskId)
});
break;
}
return state;
}
Notiamo come il parametro state, che rappresenta lo stato corrente, abbia come valore predefinito lo stato iniziale definito poco prima. Questo ci garantisce che la nostra applicazione parta sempre da uno stato consistente.
Essenzialmente la struttura del nostro reducer consiste in uno swicth
sul tipo di azione in base al quale determinare lo stato successivo da restituire. Questo è naturalmente un esempio di implementazione di un reducer.
Redux non pone vincoli particolari sull'implementazione, ma chiede soltanto che un reducer sia una funzione pura, una funzione cioè che non abbia effetti collaterali ed in particolare non modifichi l'oggetto che rappresenta lo stato dell'applicazione.
Nel nostro esempio, ciascun ramo dello switch genera lo stato successivo in base a quello attuale, ma è opportuno notare come lo stato attuale non venga modificato. Infatti il nuovo stato viene generato utilizzando Object.assign() e specificando un oggetto vuoto come primo parametro:
state = Object.assign({}, state, {todoList: ...)});
Specificando state come primo parametro avremmo generato il nuovo stato modificando quello attuale. Altra cosa importante è che in ogni caso un reducer deve restituire uno stato, anche quando viene passata un'azione che non corrisponde a nessuno dei tipi previsti. In questa situazione verrà semplicemente restituito lo stato corrente, con l'evidente significato che quella specifica azione non genera nessun cambiamento di stato.