Tempo di lettura: 14 minuti
Questa è la seconda parte della guida a React Hooks. Nella prima parte abbiamo dedicato una piccola intro alla libreria js React , gestito gli stati del componente con gli Hook di stato, abbiamo visto come funziona il mook sever Json Server per gestire un end point.
Mediante gli Hook di effetto ed alla libreria Axios siamo riusciti a gestire la chiamata asincrona all’End point.
In questo articolo andremo a completare la CRUD della demo, quindi gestendo la modifica , l’aggiunta e la cancellazione degli eroi dalla lista, delegheremo tale compito ad un service, così come avviene per Angular.
Infine aggiungeremo lo stile mediante un framework css.
React Hooks:Invio dei dati al server
Aprite il file app.js ed apportate tale modifiche:
const addHero = event => { event.preventDefault(); const heroObject = { name: newHero, date: new Date().toISOString(), important: Math.random() > 0.5, id: heroes.length + 1, } axios .post('http://localhost:3001/heroes', heroObject) .then(response => { setHeroes(heroes.concat(response.data)) setNewHero('') }) }
Creiamo un nuovo oggetto per l’eroe ma ne omettiamo la proprietà id , di solito il server lo fa per noi.L’oggetto viene inviato ad esso utilizzando il metodo axios post .
Il nuovo eroe viene aggiunto al server e quindi alla lista grazie al metodo concat di javascript il quale crea una nuova copia dell’elenco. La comunicazione fra la nostra applicazione ed il server è Asincrona, cioè ognuno ha i suoi tempi, dobbiamo noi gestire tale situazione.
Aggiungere gli eroi ai preferiti
Proseguiamo con la guida React Hooks gestendo la modifica (update) del Crud, nello specifico in questo paragrafo aggiungeremo un pulsante per ogni eroe il quale ci consentirà di aggiungerlo o meno ai preferiti.
Aprite il file Hero.js ed apportate tale modifiche:
const Hero = ({ hero, toggleImportance }) => { const label = hero.important ? 'Remove preferiti' : 'Add preferiti' return ( <li> {hero.name} <button onClick={toggleImportance}> {label} </button> </li> ) }
Abbiamo aggiunto un pulsante al componente e assegniamo il relativo gestore eventi come funzione di attivazione / disattivazione dell’importazione passata negli oggetti del componente.
Il componente App definisce una versione iniziale della funzione del gestore eventi toggleImportanceOf e la passa a ogni componente Hero :
const toggleImportanceOf = id => { const url = `http://localhost:3001/heroes/${id}` const hero = heroes.find(n => n.id === id) const changedNote = { ...hero, important: !hero.important } axios.put(url, changedNote).then(response => { setHeroes(heroes.map(hero => hero.id !== id ? hero : response.data)) }) } const rows = () => heroes.map(hero => <Hero key={hero.id} hero={hero} toggleImportance={() => toggleImportanceOf(hero.id)} > </Hero>
Ogni eroe riceve la propria funzione univoca del gestore eventi, poiché l’ id di ogni eroe è univoco.
Quasi ogni riga di codice nel corpo della funzione contiene dettagli importanti. La prima riga definisce l’URL univoco per ciascuna risorsa Hero in base al suo ID.
Il metodo find array viene utilizzato per trovare l’ eroe che vogliamo modificare, quindi lo assegniamo alla variabile hero .
Successivamente, creiamo un nuovo oggetto che è una copia esatta dell’ eroe precedente, a parte la proprietà important. Il codice per la creazione del nuovo oggetto è:
const changedHero = { ...hero, important: !hero.important }
Spread Operator di ES6
Qui ho utilizzato lo spread operator , nonostante questo non sia ancora stato standardizzato dalle speciche del linguaggio JavaScript , vale a dire che i browser più datati non lo supportano.
In pratica {… hero} crea un nuovo oggetto con copie di tutte le proprietà dall’oggetto hero . Quando aggiungiamo proprietà all’interno delle parentesi graffe dopo l’oggetto diffuso, ad esempio {… hero, important: true} , il valore della proprietà importante del nuovo oggetto sarà vero . Nel nostro esempio important ottiene la negazione del suo valore precedente nell’oggetto originale.
Abbiamo creato un nuovo oggetto in quanto non è mai raccomandabile modificare l’oggetto originario.
Il nuovo oggetto , changedHero, è solo una cosiddetta copia superficiale , il che significa che i valori del nuovo oggetto sono gli stessi dell’oggetto originario. Quindi i valori del nuovo oggetto fanno riferimento ai valori del vecchio oggetto.
Il nuovo eroe viene quindi inviato con una richiesta PUT al backend dove sostituirà il vecchio oggetto.
La funzione di callback imposta lo stato degli eroi del componente su un nuovo array che contiene tutti gli elementi precedenti , ad eccezione dell’eroe che viene sostituito dalla versione aggiornata restituita dal server:
axios.put(url, changedHero).then(response => { setHeroes(heroes.map(hero => hero.id !== id ? hero : response.data)) })
Metodo Map
Con il metodo map :
heroes.map(hero => hero.id !== id ? hero : response.data)
creiamo un nuovo array mappando ogni elemento dal vecchio array in un elemento nel nuovo array. Nel nostro esempio, il nuovo array viene creato in modo condizionale in modo che se id hero.id! == è vero, copiamo semplicemente l’elemento dal vecchio array nel nuovo array. Se la condizione è falsa, l’oggetto hero restituito dal server viene invece aggiunto all’array.
Aggiunta del service
Come ribadito più volte è bene dividere Applicazioni React in più componenti ognuno di essi con uno specifico compito. Lo stesso vale anche per la gestione delle chiamate ad un End Point, quindi la comunicazione con il server.
Per cui anche in React ,così come in Angular, è buona norma definire un service che gestisca tale compito.
Aprite l’ide ed all’interno della cartella src createvi la cartella service, all’interno de questa create il file heroService.js.
All’interno di questo inserite le seguenti righe:
import axios from 'axios' const baseUrl = 'http://localhost:3001/heroes' /*LETTURA*/ const getAll = () => { return axios.get(baseUrl) } /*AGGIUNTA*/ const create = newObject => { return axios.post(baseUrl, newObject) } /*MODIFICA*/ const update = (id, newObject) => { return axios.put(`${baseUrl}/${id}`, newObject) } export default { getAll: getAll, create: create, update: update }
Il modulo restituisce un oggetto che ha tre funzioni ( getAll , create e update ) come proprietà che gestiscono gli eroi. Le funzioni restituiscono direttamente le promises restituite dai metodi axios.
Ora importiamolo all’interno del componente App:
import React, {useState,useEffect} from 'react' import './App.css' import Hero from './components/Hero.js' import axios from 'axios' import heroService from './service/heroService'
Possiamo utilizzare i metodi del service direttamente mediante la variabile importata heroService come segue:
import React, { useState, useEffect } from 'react' import './App.css' import Hero from './components/Hero.js' import heroService from './service/heroService' const App = () => { const [heroes, setHeroes] = useState([]) const [newHero, setNewHero] = useState('') const [showAll, setShowAll] = useState(true) useEffect(() => { heroService.getAll() .then(response => { setHeroes(response.data) }) }, []) console.log('render', heroes.length, 'notes') const toggleImportanceOf = id => { const hero = heroes.find(n => n.id === id) const changedHero = { ...hero, important: !hero.important } heroService .update(id, changedHero) .then(response => { setHeroes(heroes.map(hero => hero.id !== id ? hero : response.data)) }) } const rows = () => heroes.map(hero => <Hero key={hero.id} hero={hero} toggleImportance={() => toggleImportanceOf(hero.id)} > </Hero> ) const addHero = event => { event.preventDefault(); const heroObject = { name: newHero, date: new Date().toISOString(), important: Math.random() > 0.5, id: heroes.length + 1, } heroService.create(heroObject) .then(response => { setHeroes(heroes.concat(response.data)) setNewHero('') }) } const handleHeroChange = event => { console.log(event.target.value) setNewHero(event.target.value) } return ( <div> <h1>Heroes</h1> <button onClick={() => setShowAll(!showAll)}> show {showAll ? 'important' : 'all'} </button> <ul> {rows()} </ul> <form onSubmit={addHero}> <input type='text' placeholder='Add Hero...' value={newHero} onChange={handleHeroChange} /> <button type='submit'> Add</button> </form> </div> ); } export default App;
Miglioriamo il codice
Il componete App riceve in ingresso un oggetto il quale contiene la risposta dalla richiesta Http, sarebbe opportuno lasciare tale compito al service, per cui aprite il file heroService.js e modificate come segue:
import axios from 'axios' const baseUrl = 'http://localhost:3001/heroes' const getAll = () => { const request = axios.get(baseUrl) return request.then(response => response.data) } const create = newObject => { const request = axios.post(baseUrl, newObject) return request.then(response => response.data) } const update = (id, newObject) => { const request = axios.put(`${baseUrl}/${id}`, newObject) return request.then(response => response.data) } export default { getAll: getAll, create: create, update: update }
In questo modo assegniamo la promise alla request echiamaimo il metodo then a differenza di prima in cui questa veniva restituita direttamente mediante axios:
const getAll = () => { const request = axios.get(baseUrl) return request.then(response => response.data) }
quindi modifichiamo il componente app come segue:
... useEffect(() => { heroService.getAll() .then(initialHeroes => { setHeroes(initialHeroes) }) }, []) const toggleImportanceOf = id => { const hero = heroes.find(n => n.id === id) const changedHero = { ...hero, important: !hero.important } heroService .update(id, changedHero) .then(returnedHero => { setHeroes(heroes.map(hero => hero.id !== id ? hero : returnedHero)) }) } const rows = () => heroes.map(hero => <Hero key={hero.id} hero={hero} toggleImportance={() => toggleImportanceOf(hero.id)} > </Hero> ) const addHero = event => { event.preventDefault(); const heroObject = { name: newHero, date: new Date().toISOString(), important: Math.random() > 0.5, id: heroes.length + 1, } heroService.create(heroObject) .then(returnedHero => { setHeroes(heroes.concat(returnedHero)) }) } const handleHeroChange = event => { console.log(event.target.value) setNewHero(event.target.value) } ...
inoltre nel file heroService modificate anche:
//... export default { getAll: getAll, create: create, update: update } //...
a così:
export default {getAll,create,update}
in ES6 quando, in un array , il nome della chiave è uguale al nome della proprietà il codice si può abbreviare come sopra.
Prima di andare avanti modifichiamo il componente App in modo che memorizzi un elenco di tutte gli eroi da visualizzare nella variabile heroShow. Le voci dell’elenco dipendono dallo stato del componente (cosa che non avevo fatto prima):
const rows = () => heroShow.map(hero => <Hero key={hero.id} hero={hero} toggleImportance={() => toggleImportanceOf(hero.id)} > </Hero> )
Promise ed Errori
Supponete di voler eliminare o modificare un eroe che non esiste, la promise fallirebbe ed il programma andrebbe in errore
Una promise può assumere tre diversi stati. Quando una richiesta HTTP fallisce, la promise associata viene rifiutata . Il nostro codice al momento non gestisce questa situazione.
Il rifiuto di una promise viene gestito fornendo al metodo then un secondo metodo di callback, che viene chiamato nella situazione in cui la promise viene respinta.
Tutto questo viene gestito dal metodo catch .
In pratica, il gestore degli errori per le promise respinte è definito in questo modo:
const toggleImportanceOf = id => { const hero = heroes.find(n => n.id === id) const changedHero = { ...hero, important: !hero.important } heroService .update(id, changedHero) .then(returnedHero => { setHeroes(heroes.map(hero => hero.id !== id ? hero : returnedHero)) }) .catch(error => { alert( `the hero '${hero.name}' was already deleted from server` ) setHeroes(heroes.filter(n => n.id !== id)) }) }
Al momento utilizzo un alert per mostrare l’errore, comunque nelle applicazioni serie sarebbe opportuno utilizzare altri tipi di pop pup.
Cancellare un eroe dalla lista
Aprite il service quindi il file heroService.js ed andiamo a definire il metodo che gestisce la cancellazione di un eroe:
import axios from 'axios' const baseUrl = 'http://localhost:3001/heroes' /*LETTURA*/ const getAll = () => { const request = axios.get(baseUrl) return request.then(response => response.data) } /*AGGIUNTA*/ const create = newObject => { const request = axios.post(baseUrl, newObject) return request.then(response => response.data) } /*MODIFICA*/ const update = (id, newObject) => { const request = axios.put(`${baseUrl}/${id}`, newObject) return request.then(response => response.data) } /*CANCELLAZIONE*/ const deleteHero = id => { const request = axios.delete(`${baseUrl}/${id}`) return request.then(response => response.data) } export default {getAll,create,update,deleteHero}
Come si può notare il metodo di cancellazione, deleteHero() è simile a quello di modifica con la differenza che non passo il nuovo oggetto ed ad axios ovviamente passo delete.
Aprite il componente Hero ed aggiungiamo il button che consente la cancellazione dell’Eroe in questione con il relativo evento che richiami il metodo di callback:
const Hero = ({ hero, toggleImportance,deleteHero }) => { const label = hero.important ? 'Remove preferiti' : 'Add preferiti' return ( <li> {hero.name} <button onClick={toggleImportance}> {label} </button> <button style={{ marginLeft: '5px' }} onClick={deleteHero}>delete</button> </li> ) }
Notate che ho inserito l’attributo style, argomento del prossimo paragrafo. in React lo stile in linea viene inserito fra doppie graffe, a differenza di una comune pagina html dove si inseriscono i doppi apici. Questo indica che si tratta di un oggetto e quindi di codice javascript.
Aprite il file app.js e modifichiamo come di seguito:
const deleteHero = (id, name) => { const r = window.confirm(`Sicuro di voler cancellare l'eroe ${name} ?`) if (r === false) { return } else { heroes.filter(n => n.id === id) heroService .deleteHero(id) window.location.reload() } }
ho aggiunto il metodo deleteHero() che riceve in ingresso 2 parametri che sono l’id ed il nome dell’eroe che si intende cancellare. Quindi si chiede conferma , tramite un alert dato dal metodo javascript window.confirm() (potrebbe essere fatto meglio ma accontentiamoci).
Poi utilizzo il metodo filter per filtrare i dati dell’array in base all’id passato e richiamo il calback deleteHero() dal service.
Visto che non vedo subito l’eroe eliminato dalla lista refresho la pagina in modo tale da aggiornare la lista (ovviamente non è un metodo elegante ma per ora ci accontenteremo).
Diamo un tocco di stile all’applicazione
Per quanto riguardo la gestione degli stili il modo più veloce è quello di avvalersi di un qualsiasi framework css presente oggi nel mondo web & app come Bootstrap o Materialize. Sappiate che potreste utilizzare anche stili vostri.
In questo caso opteremo per il framework Materialize di google, il quale contiene anche un ottimo set di icone che ci torneranno utili.
Aprite il file index.html presente all’interno della cartella public ed incollate i link relativi ai file css e javascipt del framework come di seguito:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> <title>React App</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> </body> </html>
visto che ci siamo copiamo ed incolliamo anche il link relativo alle icons:
... <title>React App</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> ...
ora se avviate il server ed aprite il browser di certo noterete una formattazione leggermente diversa in quanto ha preso campo lo stile di Materialize.
Aggiungi lo stile su App.js
Aprite il il componente App e modificatelo come di seguito:
... return ( <div className="container"> <div className="row"> <button className='waves-effect waves-light btn-small' onClick={() => setShowAll(!showAll)}> show {showAll ? 'important' : 'all'} </button> <h1>Heroes</h1> <ul className='collection'> {rows()} </ul> <form onSubmit={addHero} className='col s12'> <div className="input-field col s6"> <input type="text" name='name' value={newHero} onChange={handleHeroChange} required /> <label htmlFor="name">Inserisci il nome...</label> </div> <button className='btn-floating waves-effect waves-light green' type='submit'><i className="material-icons">add</i></button> </form> </div> </div> ) ...
Aggiungi lo stile al componente Hero
Ora tocca al componente Hero:
import React from 'react'; const Hero = ({ hero, toggleImportance, deleteHero }) => { // const label = hero.important // ? 'make not important' : ' make important'; return ( <li className='collection-item' style={{ borderBottom: '1px solid #dadada' }}> <span className='title'>{hero.name}</span> <span className="secondary-content"> <button className='waves-effect waves-light btn-small ' onClick={toggleImportance}>{hero.important ? 'not important' : <i className="material-icons">grade</i>}</button> <button style={{ marginLeft: '5px' }} className='btn-floating btn-small waves-effect waves-light red' onClick={deleteHero}> <i className="material-icons">delete</i></button> </span> </li> ) } export default Hero;
DEMO
Di seguito una demo live non mi ritengo responsabile dei contenuti inseriti dagli altri utenti 😉
react-2j4yef – StackBlitz
Starter project for React apps that exports to the create-react-app CLI.
Se vuoi approfondire lo studio di React puoi seguire il mio corso su Udemy: React e Redux: la guida completa.