Tempo di lettura: 10 minuti
Questa è la seconda parte della Guida Redux e React, se vi siete persi la prima parte vi invito a leggerla. La volta scorsa abbiamo studiato l’architettura di Redux e cosa sono le azioni, i reducer, creatori di azioni e store.
In questo tutorial rafforzeremo i concetti fondamentali di Redux applicando ciò che abbiamo studiato.Realizzeremo una lista di contatti.
Vi ricordo che è necessario avere quanto meno le basi di React, se volete potete seguire il corso REACT E REDUX: LA GUIDA COMPLETA.
Redux e react – Creare una lista di contatti
Realizzeremo un elenco di contatti con le seguenti caratteristiche:
- visualizzazione di tutti i contatti
- ricerca di contatti
- recuperare tutti i contatti dal server
- aggiunta di un nuovo contatto
- aggiornare un contatto
Nella prima parte ci concentreremo solo su Redux per visualizzare ed aggiungere un nuovo contatto. Inizializzeremo uno stato, creeremo lo Store, aggiungeremo Reducer ed Azioni. Nella seconda parte andremo a collegare Redux e React.
Creare uno schema ad albero dello stato
E’ buona norma avere uno schema ad albero dello stato innanzitutto, cosa che ci farà risparmiare molto tempo a lungo termine, ecco un esempio:
├── package.json ├── public ├── README.md ├── src │ ├── actions │ ├── App.js │ ├── components │ ├── containers │ ├── index.js │ ├── reducers │ └── store └──
rinominate l’applicazione realizzata la scorsa volta da shop-list a contatti, come vedete ho realizzato 5 cartelle:
- actions
- components
- containers
- reducers
- store
fatelo anche voi all’interno della directory src.
Ora all’interno della cartella Store realizzate il file index.js e digitate:
const initialState = { contacts: { contactList: [], newContact: { name: '', surname: '', email: '', address: '', phone: '' }, ui: { //All the UI related state here. eg: hide/show modals, //toggle checkbox etc. } } }
Come vedete ho realizzato uno schema ad albero dello stato della mia applicazione il che ci farà risparmiare tempo nel prosieguo dello sviluppo.
Lo store deve avere due proprietà contacts ed ui . la prima si prenderà cura di tutti i contatti relativi allo stato mentre la seconda gestisce lo stato dell’interfaccia dell’utente specifico. Tengo a precisare che in Redux non esiste una regola che ci impedisce di posizionare l’oggetto di ui come un sottostato di contacts.
Contacts ha due proprietà nidificate all’interno dello stesso contactList e newContact. La prima è un array i contatcts, mentre la seconda memorizza temporaneamente i dati del contatto durante la scrittura sul modulo conatti.
Organizzare Redux
Potete organizzare Redux come meglio vi pare per la gestione delle applicazioni , tuttavia esistono dei modelli cui è buona norma attenersi, in questo tutorial noi seguiremo quello relativo allo stile Rails. Difatti vi ho già fatto creare quelle famose 5 cartelle viste in precedenza:
- components : dove memorizzeremo i componenti Dumb di React, questi non prescindono da Redux.
- containers: la directory che contiene i componenti smart di React, quella che invia azioni allo store di Redux. L’associazione di Redux e React avviene in questa cartella.
- actions: all’interno vi saranno gli action creators.
- reducers: come si intuisce è la directory che conterrà la logica dei Reducers.
- store: directory che contiene la logica per l’inizializzazione dello stato e la configurazione dello store.
La seguente immagine riepiloga il tutto:
Redux e React: Store singolo, reducers multipli
Creiamo un prototipo per store e reducer, visto l’esempio precedente , ecco come apparirebbe lo store:
const store = createStore( reducer, { contacts: { contactlist: [], newContact: { } }, ui: { isContactFormHidden: true } }) const reducer = (state, action) => { switch(action.type) { case "HANDLE_INPUT_CHANGE": break; case "ADD_NEW_CONTACT": break; case "TOGGLE_CONTACT_FORM": break; } return state; }
L’istruzione switch ha tre casi che corrispondono alle tre azioni che creeremo:
- HANDLE_INPUT_CHANGE: azione che viene attivata nel momento in cui l’utente inserisce nuovi valori nel modulo.
- ADD_NEW_CONTACT: questa si attiva quando l’utente invia il modulo.
- TOGGLE_CONTACT_FORM: questa è un’azione dell’interfaccia utente che si occupa di mostrare o nascondere il modulo dei contatti.
anche se questo semplice approccio funziona, man mano che l’applicazione cresce ciò comporterà alcune lacune, in quanto:
- Stiamo utilizzando un solo reducer. Anche se al momento può andar bene , immaginate un’applicazione di modeste dimensioni e di inserire tutta la logica su di un singolo reducer…
- In effetti il codice riportato sopra non rispecchia la struttura discussa in precedenza.
Per risolvere tali problematiche Redux ci mette a disposizione un metodo denominato combineReducers il quale ci consente di creare più reducer per poi unirli in una singola funzione. Questa, indubbiamente, migliora la leggibilità. Quindi dividiamo il reducer in due:
- contactsReducer
- uiReducer
Nel precedente esempio , createStore accetta un secondo argomento opzionale, cioè lo stato iniziale.
Se ci accingiamo a dividere i reducer, possiamo passare tutto initialState in una nuova posizione nel file , ad esempio in initialState.js. dopo di che importiamo importiamo un sottoinsieme dello stesso in ogni file di reducer.
dividere il reducer
Ristrutturiamo il codice per risolvere entrambi i problemi. Create un nuovo file nella cartella store e rinominatelo con createStore.js, quindi scrivete:
import {createStore} from 'redux'; import rootReducer from '../reducers/'; /*Create a function called configureStore */ export default function configureStore() { return createStore(rootReducer); }
quindi create anche un file per il reducer radice , reducers/index.js e scrivete:
import { combineReducers } from 'redux' import contactsReducer from './contactsReducer'; import uiReducer from './uiReducer'; const rootReducer =combineReducers({ contacts: contactsReducer, ui: uiReducer, }) export default rootReducer;
Ora abbiamo bisogno di creare il codice per contactsReducer e uiReducer.
reducers/contactsReducer.js
import initialState from './initialState'; export default function contactReducer(state = initialState.contacts, action) { switch(action.type) { /* Add contacts to the state array */ case "ADD_CONTACT": { return { ...state, contactList: [...state.contactList, state.newContact] } } /* Handle input for the contact form. The payload (input changes) gets merged with the newContact object */ case "HANDLE_INPUT_CHANGE": { return { ...state, newContact: { ...state.newContact, ...action.payload } } } default: return state; } }
reducers/uiReducer.js
import initialState from './initialState'; export default function uiReducer(state = initialState.ui, action) { switch(action.type) { /* Show/hide the form */ case "TOGGLE_CONTACT_FORM": { return { ...state, isContactFormHidden: !state.isContactFormHidden } } default: return state; } }
tenete sempre presente che un reducer deve sempre avere un valore predefinito per quanto riguarda il proprio stato e che deve sempre restituire qualche cosa, diversamente otterremo un errore.
Riepiloghiamo quanto fatto
- La chiamata combineReducers serve a legare i reducer divisi.
- lo stato di ui è affidato a uiReducer e lo stato dei contatti a contactsReducer.
- Per mantenere i reducer puri abbiamo utilizzato lo spread operator , il quale crea una copia dell’array quindi non modifica l’originale (vedi qui).
- Il valore iniziale non è più specificato come argomento facoltativo di createStore. Difatti abbiamo creato un file separato initialstate.js. Importo initialState ed imposto lo stato predefinito con state = initialState.ui.
inizializzare lo stato
create il file reducers/initialState.js.
const initialState = { contacts: { contactList: [], newContact: { name: '', surname: '', email: '', address: '', phone: '' }, }, ui: { isContactFormHidden: true } } export default initialState;
Redux e React: Actions e actions creators
Aggiungiamo un paio di Actions e di Actions creators in modo tale da poter gestire le modifiche provenienti dal modulo web. Aggiungete un nuovo contatto ed attivazione/disattivazione dello stato dell’interfaccia utente. Se vi ricordate i creatori di azioni (actions creators) sono delle semplici funzioni che restituiscono un’azione.
createvi actions/index.js ed aggiungete:
export const addContact =() => { return { type: "ADD_CONTACT", } } export const handleInputChange = (name, value) => { return { type: "HANDLE_INPUT_CHANGE", payload: { [name]: value} } } export const toggleContactForm = () => { return { type: "TOGGLE_CONTACT_FORM", } }
ogni azione deve restituire una proprietà del type. Il type è come una chiave che determina quale reducer chiamare e come lo stato viene aggiornato in risposta a tale azione. Il payload è facoltativo.
Nel caso nostro abbiamo realizzato tre azioni.
TOGGLE_CONTACT_FORM in effetti non ha bisogno di un payload in quanto ogni qual volta l’azione viene attivata, il valore di ui.isContactsFormHidden viene attivato o disattivato. Le azioni con valore booleano non richiedono payload.
L’azione HANDLE_INPUT_CHANGE viene attivata nel momento in cui cambia il valore nel modulo. Immaginate che l’utente stia riempiendo il campo email, l’azione riceve “e-mail” e “tuaemail@email.it” come input ed il payload consegnato al reducer è un oggetto che assomiglia a questo:
{ email: "tuaemail@email.it" }
il reducer utilizza tali informazioni per aggiornare le proprietà pertinenti dello stato newContact.
Inviare le azioni e ricevere notifiche dallo store
Il prossimo passo è quello di inviare le azioni. Una volta che queste vengono inviate lo stato cambia in funzione di queste. Se vi ricordate i metodi per inviare azioni e ricevere risposte sono :
- dispatch(Action): Invia un’azione la quale innesca un cambiamento di stato.
- getState(): restituisce lo stato corrente dell’applicazione.
- subscriber(listner): listner di modifica il quale viene chiamato in causa ogni volta che un’azione viene inviata ed una parte dell’albero dello stato è cambiata.
Aprite il file src/index.js e digitate:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
quindi importatela funzione configureStore e le tre azioni che abbiamo creato in precedenza:
... /* Import Redux store and the actions */ import configureStore from './store/createStore'; import {toggleContactForm, handleInputChange} from './actions'; ...
ora create un oggetto store ed aggiungete il listner che modifichi lo stato ogni qual volta viene inviata un’azione:
const store = configureStore(); //Note that subscribe() returns a function for unregistering the listener const unsubscribe = store.subscribe(() => console.log(store.getState()) )
quindi inviamo qualche azione:
/* returns isContactFormHidden returns false */ store.dispatch(toggleContactForm()); /* returns isContactFormHidden returns false */ store.dispatch(toggleContactForm()); /* updates the state of contacts.newContact object */ store.dispatch(handleInputChange('email', 'info@freewebsolution.it'))
se tutto è andato bene dovreste verdere:
nella console del browser noterete che lo store cambia ad ogni azione inviata.
Riepilogo
Abbiamo creato una piccola applicazione Redux dove abbiamo visto come funziona il reducer e come possa essere diviso in modo tale da rendere la struttura dell’app più pulita e performante. Inoltre si noti come le azioni riescono a mutare lo stato nello store.
Grazie al metodo store.subscribe() abbiamo mutato lo store. Ovviamente questo non è il miglior metodo di sviluppo Redux abbinato a React. Nel prossimo tutorial collegheremo il FRONT END di React a Redux 🙂 .