Redux in Vanilla Javascript

Maico Orazio
weBeetle
Published in
9 min readMar 24, 2021

Redux, libreria JavaScript open source, ad oggi è cambiata molto e il miglior modo per utilizzarla all’interno delle applicazioni è attraverso Redux Toolkit

Nonostante i cambiamenti, gli elementi fondamentali di Redux sono ancora store, action, reducer e middleware ed è quindi necessaria una buona conoscenza di questi blocchi prima di iniziare con Redux e Redux Toolkit.

Questa guida ha lo scopo di introdurre Redux con React, vista l’ampia adozione di questa combinazione, ma anche di Redux in forma stand-alone, poiché può anche essere usato senza alcun framework / libreria frontend.

In questa prima parte, approfondirò l’utilizzo di Redux in forma stand-alone, poi parlerò dell’utilizzo in React, successivamente eseguiremo il refactoring in Redux Toolkit.

Cos’è Redux?

Prima di rispondere a questa domanda, dobbiamo parlare dello stato nelle applicazioni web in JavaScript.

In generale, una tipica applicazione JavaScript è piena di stato, come:

  • i dati visualizzati nell’app
  • i dati che recuperiamo da fonti esterne, ad esempio API
  • gli elementi selezionati all’interno di una pagina
  • eventuali errori da mostrare all’utente

Anche dietro a una banale interazione con l’applicazione, come ad esempio il click su un pulsante che apre la modale, può esserci uno stato con cui confrontarci

Ad esempio, possiamo descrivere lo stato iniziale come un semplice oggetto JavaScript:

const state = {
modalOpen: false
}

quando l’utente clicca sul pulsante, lo stato cambia in:

const state = {
modalOpen: true
}

Se hai già lavorato con React, non dovresti sorprenderti.

Immagino che tu abbia già scritto un componente React “stateful”, in cui si definisce lo stato interno del componente con i dati che possono essere resi all’utente e che possono cambiare in risposta ad azioni ed eventi. Un componente React Stateful è un classe JavaScript, con i React Hooks non è più così.

Redux è una libreria JavaScript per la gestione dello stato, per stato delle applicazioni web si intende tutto ciò che abbiamo elencato sopra, in generale tutti i dati e le informazioni da visualizzare o che devono essere disponibili all’intera applicazione.

A cosa serve Redux?

Abbiamo detto che lo stato è ovunque in un’applicazione web. Anche per una semplice app a pagina singola, che con il tempo potrebbe crescere, le cose diventerebbero più complicate da gestire e, ad un certo punto, potremmo voler raggiungere un modo coerente per tener traccia dei cambiamenti di stato, centralizzando la sua logica senza averla nei diversi componenti.

Redux può risolvere questi problemi: aiuta nel dare a ciascun componente lo stato esatto di cui ha bisogno e a contenere la logica dei cambiamenti di stato attraverso i middleware, insieme al codice per il recupero dei dati.

Esistono diverse alternative a Redux e molti modi per evitarlo, che affronteremo in un altro articolo, ma penso sia obbligatorio per progetti di dimensioni medio-grandi, in modo da mantenere la gestione dello stato astratta dall’interfaccia utente.

Pregi:

  • Universale
  • Time travel: possiamo vedere cosa ha fatto l’utente o cosa è successo nello stato dell’applicazione come in una macchina del tempo

Difetti:

  • Verboso: è considerato molto verboso come libreria, nel senso che per aggiungere una semplice interazione bisogna scrivere codice in diverse parti dell’applicazione; difetto risolto con l’introduzione di Redux Toolkit

Gli elementi fondamentali in Redux — Architettura

Gli elementi fondamentali in Redux sono sostanzialmente tre: Store, Reducer e Actions.

Store

Iniziamo con quello principale, da considerarsi come il cervello di Redux, ovvero lo Store, che contiene tutto lo stato dell’applicazione. Per iniziare a giocare con Redux, bisogna creare lo store, ma prima includiamo la libreria nella nostra pagina html dell’applicazione:

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script>
const store = Redux.createStore(rootReducer);
</script>

La funzione createStore prende un reducer come primo argomento, che per convenzione chiamiamo rootReducer, e restituisce lo store. Il reducer, produttore del nuovo stato dell’applicazione, non va chiamato come una funzione, ma solo passato come referenza, perché poi Redux si occuperà di chiamarlo per conto suo durante l’esecuzione dell’applicazione

Reducer

Il principio fondamentale di Redux è che lo stato è immutabile e non può cambiare. Per questo motivo in Redux abbiamo il reducer: una funzione pura che restituisce lo stesso identico output per un’input specifico.

Un reducer è una funzione JavaScript che prende come parametri, lo stato iniziale e la action e produce il nuovo stato:

const initialState = {
todos: [];
}
function rootReducer(state = initialState, action) {
return state;
}

Lo stato iniziale dell’applicazione initialState viene passato come parametro predefinito, ma per ora il nostro reducer non fa altro che restituire lo stato iniziale.

E’ possibile combinare più reducers con la funzione combineReducers, che permette di definire un oggetto dove ogni proprietà corrisponde al nome di una parte dello store di Redux, prodotta da uno specifico reducer:

const initialState = {
todos: [];
}
function todoReducer(state = initialState, action) {
return state;
}
function loggedReducer(state = false, action) {
return state;
}
const rootReducer = Redux.combineReducers({
todos_data: todoReducer,
user_data: loggedReducer
});
const store = Redux.createStore(rootReducer);

Actions e Named Constants

Come abbiamo detto e sottolineato, il reducer produce lo stato di un’applicazione, ma come fa un reducer a sapere quando generare lo stato successivo?

Il secondo principio di Redux dice che l’unico modo per cambiare lo stato è inviare un messaggio allo store, questo messaggio è una action. Le actions non sono altro che oggetti JavaScript, che per convenzione, hanno due proprietà: type e payload.

{
type: "ADD_TODO",
payload: {title: "Primo task", id: 1}
}

La proprietà type determina il modo in cui lo stato dovrebbe cambiare ed è sempre richiesta da Redux. La proprietà payload descrive invece la parte dello stato che deve cambiare.

Come best practice in Redux, racchiudiamo ogni action in una funzione, in modo che la creazione dell’oggetto venga astratta; questa funzione viene chiamata action creator.

function addTodo(payload) {
return {
type: "ADD_TODO",
payload
}
}

La tipologia della action viene definita tramite la proprietà type che, come riportato nell’esempio sopra, è rappresentata da una stringa.

Come vedremo, il tipo di ogni action viene ripetuto nel codice diverse volte e, se pensiamo ad una applicazione medio-grande, è abbastanza facile sbagliare mentre scriviamo la stringa che la identifica, commettendo errori banali, non semplici da debuggare, su cui potremmo perdere diverse ore a cercare questo tipo di inconsistenze nel codice.

Introduciamo un’altra best practice: consiste nel prendere queste stringhe e isolarle all’interno di costanti, che andremo ad utilizzare al posto delle stringhe named constants.

const ADD_TODO = "ADD_TODO;function addTodo(payload) {
return {
type: ADD_TODO,
payload
}
}

Condivido una nota a margine: stiamo scrivendo tanto codice in diversi blocchi ognuno con la propria funzionalità, questa è il principale motivo per cui molte persone sono contrarie all’utilizzo di Redux.

Come cambiamo lo state?

Abbiamo scritto che è il reducer a sapere quando e come generare il nuovo stato dell’applicazione. Quando viene inviato un messaggio allo store, quest’ultimo inoltra il messaggio, ovvero l’oggetto action, al reducer.

A seconda del tipo di action (proprietà type), il reducer produce lo stato successivo combinando i dati (proprietà payload) nel nuovo stato.

const initialState = {
todos: [];
}
function rootReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
state.todos.push(action.payload); // NON CORRETTO!!! FIX
return state;
default:
return state;
}
}

La riga di codice che aggiunge i dati della action nello stato è errata; questo errore infrange il principio fondamentale di Redux: l’immutabilità.

Inoltre, si perde uno dei vantaggi di redux riportato all’inizio dell’articolo, ovvero il time travel, per cui non potremmo vedere quali sono state le modifiche allo stato nel corso dell’applicazione.

Abbiamo detto che il reducer è una funzione pura, mentre la funzione che abbiamo utilizzato per generare il nuovo stato Array.prototype.push è impura, in quanto modifica l’array originale, oltre a cambiare lo stato iniziale dell’applicazione.

Una soluzione a questo problema, può essere data dall’utilizzo di Object.assign, che restituisce un nuovo oggetto, in modo da mantenere inalterato lo stato iniziale, e usare Array.prototype.concat per mantenere l’array originale.

const initialState = {
todos: [];
}
function rootReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: state.todos.concat(action.payload);
});
default:
return state;
}
}

Ora lo stato risultante è solo una copia di quello iniziale.

Object.assign è un metodo javascript che prende un oggetto vuoto come primo argomento, l’oggetto che poi sarà ritornato all’esterno; come secondo argomento un altro oggetto, quello da cui partire; come terzo argomento, l’oggetto che contiene le modifiche.

Esiste un altro stile che utilizza l’object spread però, a meno che non utilizziate babel, questa sintassi non funziona nei browser più vecchi:

return {...state, todo: state.todos.concat(action.payload)};

L’object spread prevede, in pratica, di passare all’interno di un oggetto vuoto, lo stato da cui partire e la proprietà di cui vogliamo modificarne il valore. Succede che l’oggetto nuovo, ovvero quello all’esterno, viene esploso all’interno dell’oggetto passato come primo argomento- state- e qualsiasi proprietà indicata dopo sovrascrive il valore presente in state, che rimane sempre invariato.

I principali metodi dello store di Redux

I metodi più importanti dello store di Redux sono solo tre:

  • getState, per leggere lo stato corrente dell’applicazione;
  • dispatch, per l’invio della action;
  • subscribe, per mettersi in ascolto dei cambiamenti di stato.

Per cambiare lo store di Redux dobbiamo inviare una action.Per fare questo utilizzeremo il metodo dispatch, che prende come parametro la funzione di action creator a cui possiamo passare i dati che vogliamo modificare.

L’interfaccia, per mettersi in ascolto degli aggiornamenti dello stato, utilizza il metodo subscribe dello store, ovvero si iscrive alle notifiche di cambiamento dello stato dell’applicazione. Questo metodo prende come parametro una callback, richiamata ogni volta che viene inviata una action, e dove viene implementata la logica di aggiornamento dell’interfaccia.

store.dispatch(addTodo({title: "Primo task", id: 1}));store.subscribe(function(){
console.log('stato app', store.getState());
});

Che cos’è un middleware Redux?

Il middleware Redux è una funzione JavaScript in grado di intercettare e, di conseguenza, agire sulle actions prima che raggiungano il reducer. Un modo per attingere al flusso dell’applicazione.

Nella sua forma base un middleware Redux è una funzione che restituisce un’altra funzione, che prende next come parametro; questa funzione interna ne restituisce un’altra che prende action come parametro e infine restituisce next(action):

function loggerMiddleware(store) {
return function (next) {
return function (action) {
console.info(action) // logga la action inviata
return next(action)
}
}
}

Il middleware deve sempre ritornare next(action), altrimenti Redux si ferma e nessun’altra action raggiungerà il reducer. next(action) manda l’applicazione “avanti” chiamando il middleware successivo nella lista.

La cosa interessante è che alla funzione del middleware viene passato lo store di Redux, quindi è possibile accedere allo stato corrente dell’applicazione tramite il metodo store.getState e inviare le actions tramite il metodo store.dispatch.

Ci sono diversi vantaggi nell’usare un middleware Redux:

  • il middleware diventa un pezzo di logica riutilizzabile;
  • il middleware può essere testato in modo isolato.

Il nostro primo middleware Redux

Prima abbiamo visto il codice del middleware loggerMiddleware, la cui funzione è di loggare sulla console qualsiasi action inviata dall’applicazione.

middleware loggerMiddleware in azione
middleware loggerMiddleware in azione

Creiamo il nostro primo middleware Redux che implementa la logica di controllo del testo inviato come titolo dell’attività da aggiungere all’elenco delle todos

const wordsNotAllowed = ["spam", "subito", "urgente"];function moderatorMiddleware(store) {
return function (next) {
return function (action) {
if (action.type == ADD_TODO) {
const todo = action.payload;
const foundWord = wordsNotAllowed.filter(word => todo.title.includes(word));
if (foundWord.length) {
return store.dispatch({type:"FOUND_WORD"});
}
}
return next(action)
}
}
}

Quando il tipo di action è ADD_TODO controlla se il titolo dell’attività contiene almeno una delle parole vietate wordsNotAllowed; nel caso le contenesse invia una action di tipo FOUND_WORD, altrimenti lascia che il successivo middleware venga eseguito.

Per collegare i middleware allo store di Redux, dobbiamo utilizzare la funzione applyMiddleware, che permette di applicare allo store uno o più middleware passati come parametri. L’ordine di inserimento è utilizzato da Redux per lanciarli quando l’applicazione effettua l’invio di una action. Ogni middleware successivo vedrà la action modificata dal precedente middleware:

const store = Redux.createStore(rootReducer, Redux.applyMiddleware(loggerMiddleware, moderatorMiddleware));

Con quest’ultimo concetto abbiamo definito tutti gli elementi fondamentali di Redux.

Questo è Redux nella sua forma più semplice e utilizzabile in modalità standalone. Gli esempi riportati nell’articolo sono stati scritti e definiti nel modo più veloce e semplice, utili per raggiungere lo scopo di farvi capire Redux e come tutti gli elementi lavorano insieme.

Trovate nel mio repository GitHub l’esempio completo.

--

--

Maico Orazio
weBeetle

Senior Web Application Developer. I'm a software engineer, a passionate coder, and a web developer. I am a fan of technology. #php #symfony #javascript #reactjs