Tempo di lettura: 9 minuti
Nei tutorial precedenti abbiamo visto come funziona Redux e creato una piccola applicazione. In questa guida ci accingeremo a creare un collegamento React Redux ed andremo a completare l’applicazione iniziata la volta scorsa.
Se non lo avete fatto, prima di proseguire, vi consiglio di leggervi gli articoli precedenti:
React Redux: componenti smart e componenti dumb
Anche se si tratta di un argomento affrontato nel corso del primo tutorial diamo un veloce sguardo alle differenze tra i due componenti. Se vi ricordate vi ho fatto creare due cartelle distinte ossia containers per i componenti smart e components per i dumb.
Tale approccio offre il vantaggio di separare la logica di comportamento dalla vista.
I componenti di presentazione sono definiti dumb in quanto si preoccupano della vista dell’App e non della logica della stessa. Questi ricevono proprietà e/o metodi dal componente genitore tramite le props (se non capite di cosa parlo vi consiglio di seguire questa guida).
Vediamo ora come organizzare il nostro proggetto:
I componenti Dumb o presentazionali
Ecco i componenti di presentazione che utilizzeremo:
components/addContactForm.js
import React from 'react'; const AddContactForm = ({onInputChange, onFormSubmit}) => ( <form> <div className="form-group"> <label htmlFor="emailAddress">Email address</label> <input type="email" class="form-control" name="email" onChange={onInputChange} placeholder="name@example.com" /> </div> {/* Some code omitted for brevity */} <div className="form-group"> <label htmlFor="physicalAddress">Address</label> <textarea className="form-control" name="address" onChange={onInputChange} rows="3"></textarea> </div> <button type="submit" onClick={onFormSubmit} class="btn btn-primary"> Submit </button> </form> ) export default AddContactForm;
come potete notare utilizzo classi di bootstrap quindi aprite il file public/index.html ed aggiungete:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
subito prima della chiusura del tag head.
Si tratta di un semplice modulo HTML per aggiungere un nuovo contatto. Il componente riceve il callback onInoputChange e onFormSubmit come props. L’evento onInputChange viene generato quando cambia il valore di input e onFormSubmit quando il modulo viene inviato.
components/ContactList.js
const ContactList = (props) => { return( <ul className="list-group" id="contact-list"> {props.contactList.map( (contact) => <li key={contact.email} className="list-group-item"> <ContactCard contact = {contact}/> </li> )} </ul>) } export default ContactList;
Questo componente riceve un array di oggetti di contatto come props. Quindi utilizzo il metodo array.map() per estrarre i singoli dettagli e trasmetterli a <ContactCard/>.
components/ContactCard.js
const ContactCard = ({contact}) => { return( <div> <div className="col-xs-4 col-sm-3"> {contact.photo !== undefined ? <img src={contact.photo} alt={contact.name} className="img-fluid rounded-circle" /> : <img src="img/profile_img.png" alt ={contact.name} className="img-fluid rounded-circle" />} </div> <div className="col-xs-8 col-sm-9"> <span className="name">{contact.name + ' ' + contact.surname}</span><br/> {/* Some code omitted for brevity */} </div> </div> ) } export default ContactCard;
Questo riceve un oggetto di contatto e visualizza il nome e l’iimagine del contatto.
React Redux: i componenti Smart o containers
Vediamo quali saranno i componenti Smart o containers.
containers/Contact.js
class Contacts extends Component { constructor(props) { super(props); this.returnContactList = this.returnContactList.bind(this); } returnContactList() { // Retrieve contactlist from the store } render() { return ( <div> <AddContact/> <br /> <ContactList contactList= {this.returnContactList()} /> </div> ); } } export default Contacts;
Il metodo returnContactList() recupera l’array degli oggetti di contatto e lo passa al componente ContactList. Poichè returnContactList() recupera i dati dallo store, al momento lo lasceremo in bianco.
containers/AddContact.js
class AddContact extends Component { constructor(props) { super(props); /* Function binding goes here. Omitted for brevity */ } showAddContactBox() { /* Logic for toggling ContactForm */ } handleInputChange(event) { const target = event.target; const value = target.value; const name = target.name; /* Logic for handling Input Change */ } handleSubmit(e) { e.preventDefault(); /* Logic for hiding the form and update the state */ } /* Renders the AddContactForm */ renderForm() { return( <div className="col-sm-8 offset-sm-2"> <AddContactForm onFormSubmit={this.handleSubmit} onInputChange={this.handleInputChange} /> </div> ) } render() { return( <div> { /* A conditional statement goes here that checks whether the form should be displayed or not */} </div> ) } } export default AddContact;
Abbiamo creato tre componenti i quali gestiscono l’invio delle azioni per aggiornare lo stato. Al momento il metodo render() non contiene niente in quanto abbiamo prima bisogno di recuperare lo stato.
Libreria React-Redux
Per poter collegare React e Redux dobbiamo prima installare una libreria supplementare chiamata react-redux.
Aprite il terminale e digitate:
$ npm i --save react-redux
Questa esporta solamente due API, un componente <Provider/> ed una funzione connect().
Il componente Provider
Il pattern Provider consente di passare lo stato di tutti i componenti che fanno parte dell’albero dell’applicazione quindi dimezzandoci il lavoro, notate il codice sotto:
codice demo
import { Provider } from 'react-redux' ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
Avvolgiamo il provider intorno al componente dell’App visto che l’intera applicazione deve poter accedere allo store. In questo modo tutti i componenti hanno accesso ai dati.
Metodo connect()
Una volta messo a disposizione lo store all’applicazione dobbiamo collegare React con questo. L’unico modo per comunicare con lo store è quello di inviare azioni e di recuperare lo stato. In precedenza abbiamo visto come fare utilizzando i metodi store.dispatch() per inviare azioni e store.getState() per recuperare l’ultimo snapshot dello stato.Il metodo connect() ci consente di fare la medesima cosa con l’ausilio di due metodi denominati mapDispatchToProps e mapStateToProps. Vediamo l’esempio sotto:
import {connect} from 'react-redux' const AddContact = ({newContact, addContact}) => { return ( <div> {newContact.name} <br /> {newContact.email} <br /> {newContact.phone} <br /> Are you sure you want to add this contact? <span onClick={addContact}> Yes </span> </div> ) } const mapStateToProps = state => { return { newContact : state.contacts.newContact } } const mapDispatchToProps = dispatch => { return { addContact : () => dispatch(addContact()) } } export default connect( mapStateToProps, mapDispatchToProps )(AddContact)
mapStateToProps e mapDispatchToProps restituiscono entrambi un oggetto e la chiave di questo oggetto diventa una props del componente collegato. Per esempio , state.contacts.newContact è mappata a props.newContact. L’action creator addContact() viene mappato a props.addContact.
Infine la riga :
export default connect( mapStateToProps, mapDispatchToProps )(AddContact)
ci consente di esportare direttamente il componente AddContact. Questa ci fornisce addContact e newContact come props per il componente <AddContact/>.
Come connettere React e Redux
Bene una volta installato la libreria react-redux aprite il file src/index.js per avvolgere, come detto in precedenza, il Provider intorno al componente radice App:
src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux' import App from './App'; import configureStore from './store/createStore'; const store = configureStore(); ReactDOM.render( <Provider store={store}> <App /> </Provider> , document.getElementById('root'));
Connettere i connettori di React a Redux
La funzione di connessione connect() consente l’associazione del contenitore di React a Redux. Quindi questa verrà utilizzata per :
- Sottoscrizione dello store e mapping dello stato alla props
- Invio azioni e mappare i callback di dispatch alle props
Una volta collegata l’applicazione a Redux , è possibile utilizzare this.props per accedere allo stato attuale e spedire azioni. Vediamo quindi come funziona il componente AddContact. Questo mi deve inviare tre azioni ed ottenere lo stato di due proprietà dallo store:
AddContact.js
Innanzitutto importate connect in AddContact.js
import {connect} from 'react-redux';
quindi realiziamo i metodi mapStateToProps e mapDispatchToProps all’interno del costrutture della classe ovviamente.
function mapStateToProps(state) { return { isHidden : state.ui.isAddContactFormHidden, newContact: state.contacts.newContact } } function mapDispatchToProps(dispatch) { return { onFormSubmit: (newContact) => { dispatch(addContact(newContact)); }, onInputChange: (name,value) => { dispatch(handleInputChange(name,value)); }, onToggle: () => { dispatch(toggleContactForm()); } } }
mapStateToProps riceve lo stato dallo store come argomento. Restituisce un oggetto che descrive come l stato dello store viene mappatonel vostro props. mapDispatchToProps restituisce un oggetto simile il quale descrive in che modo vengono mappate le azioni dispatch sulla props.
In fine utilizziamo connect per associare il componente AddContact alle due funzioni come di seguito:
export default connect(mapStateToProps, mapDispatchToProps) (AddContact)
Aggiornare i componenti del contenitore per utilizzare i props
Le props del componente sono in grado ora di leggere lo stato dallo store e dalle azioni dispatch. Quindi aggiorniamo il componente modificando la logica dei metodi handleInputChange , handleSubmit e showAddContactBox come segue:
showAddContactBox() { const { onToggle } = this.props; onToggle(); } handleInputChange(event) { const target = event.target; const value = target.value; const name = target.name; const { onInputChange } = this.props; onInputChange(name,value); } handleSubmit(e) { e.preventDefault(); this.props.onToggle(); this.props.onFormSubmit(); }
Una volta, quindi, definiti i metodi del gestore handler non ci resta aggiorante il metodo render() inserendo un istruzione condizionale che ci mostri o meno il modulo contatti:
render() { return( <div> { this.props.isHidden === false ? this.renderForm(): <button onClick={this.showAddContactBox} className="btn"> Add Contact </button>} </div> ) }
Se isHidden è false ,il modulo viene eseguito. In caso contrario viene mostrato un pulsante.
Visualizzare i contatti
La parte più impegnativa è stata conclusa, ora non ci resta che visualizzare i contatti come un elenco. Modifichiamo containers/Contacts.js come segue:
import React, { Component } from 'react'; import { connect } from 'react-redux'; /* Component import omitted for brevity */ class Contact extends Component { constructor(props) { super(props); this.returnContactList = this.returnContactList.bind(this); } returnContactList() { return this.props.contactList; } render() { return ( <div> <br /> <AddContact/> <br /> <ContactList contactList= {this.returnContactList()} /> </div> ); } } function mapStateToProps(state) { return { contactList : state.contacts.contactList, } } export default connect(mapStateToProps, null) (Contact);
La funzione mapStateToProps mappa l’oggetto dello store ai props contactList. Quindi ho utilizzato connect per associare il valore di props al componente Contacts. Il secondo argomento di connect è null , perchè non abbiamo azioni da inviare al momento.
Dichiarare le azioni come costanti
Ho ribadito più volte che le proprietà type sono stringhe e come tali sono soggette ad errori di battitura. Quindi è una best practice dichiararle come costanti, create una cartella:
mkdir -p src/costants
ora aprite la cartella appena creata e createvi il file action-type.js:
export const ADD_CONTACT = "ADD_CONTACT"; export const HANDLE_INPUT_CHANGE ="HANDLE_INPUT_CHANGE"; export const TOGGLE_CONTACT_FORM ="TOGGLE_CONTACT_FORM"
aprite il file actions/index.js ed aggiornate come segue:
import {ADD_CONTACT} from '../costants/action-type'; import { HANDLE_INPUT_CHANGE } from '../costants/action-type'; import { TOGGLE_CONTACT_FORM } from '../costants/action-type'; 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 } }
come vedete ho importato le costanti create in precedenza ed anzi che utilizzare le proprietà type come stringhe utilizzo le costanti ciò mi consente un auto complete riducendo il rischio di commettere errori.