back to top

React app e comunicazione con un server

In questo articolo vediamo come sia semplice comunicare con un server e utilizzare l’API fornita per salvare lo stato della nostra applicazione in modo da ripristinarlo ogni volta che effettuiamo un refresh della pagina. Infatti, finora l’oggetto State dei nostri componenti manteneva le informazioni in memoria fino al prossimo refresh della pagina del browser. Ogni volta che avviavamo nuovamente l’applicazione, tutte le informazioni venivano perse.

In questo articolo realizzeremo una semplice applicazione come quella mostrata nell’immagine sottostate. Abbiamo una lista di supereroi che possiamo espandere. Possiamo inoltre rimuovere i singoli elementi dalla lista.

wireframe applicazione react lista supereroi

Utilizzeremo una serie di package che potremo aggiungere al nostro progetto col comando npm. Useremo innanzitutto JSON Server che ci permetterà di usare rapidamente una fake REST API creando un singolo file db.json e uniqid, grazie al quale potremo generare con un singolo comando degli id unici in un formato specifico. Aggiungiamo anche concurrently in modo da lanciare contemporaneamente la nostra applicazione React e json-server.

Configurazione e installazione dipendenze

Per prima cosa creiamo un nuovo progetto.

$ create-react-app react-superheroes-app

Aggiungiamo ora i tre package JSON Server, uniqid e concurrently.

# installiamo json-server globalmente
$ npm install -g json-server

# All'interno della cartella react-superheroes-app
# installiamo localmente uniqid e concurrently
$ npm install uniqid concurrently --save

Modifichiamo quindi il file package.json che conterrà le seguenti informazioni. Notate che abbiamo modificato leggermente la sezione scripts in modo da avviare CRA e JSON-server tramite Concurrently. In particolare verrà lanciato webpack-dev-server sulla porta 3000 e JSON server sulla porta 3001.

{
  "name": "react-superheroes-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "concurrently": "^3.4.0",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "uniqid": "^4.1.1"
  },
  "devDependencies": {
    "react-scripts": "0.9.5"
  },
  "scripts": {
    "start": "concurrently --kill-others "npm run start-react" "npm run json-server"",
    "start-react": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "json-server": "json-server -p 3001 --watch db.json"
  }
}

Aggiungiamo quindi alcune directory al nostro progetto. Potete vedere la struttura delle cartelle nell’immagine sottostate. Notate che abbiamo aggiunto una directory components e una utils. Nella prima salveremo i file contenenti i diversi componenti della nostra applicazione, nella seconda aggiungeremo i file in cui scriveremo alcune funzioni che ci saranno utili nel corso dell sviluppo della nostra applicazione. Abbiamo inoltre rimosso i file di cui non avevamo bisogno.

struttura directory applicazione react lista supereroi

Abbiamo anche aggiunto un file db.json con il seguente contenuto. Il file sarà utilizzato da JSON-server.

{
  "superheroes": [
    {
      "id": "hero_j1m8m4b",
      "name": "Captain America"
    },
    {
      "id": "hero_j1m8oq0c",
      "name": "Iron Man"
    },
    {
      "id": "hero_j1m8p3fd",
      "name": "Hulk"
    }
  ]
}

Lanciamo il comando npm start e nel terminale dovreste vedere un output simile a quello mostrato qui sotto.

output console concurrently

Sul lato sinistro dell’immagine potete notare un’alternanza di ‘[0]’ e ‘[1]’. È l’output generato da concurrently, dato che abbiamo eseguito due comandi concorrentemente. Ogni volta che il processo, lanciato con react-scripts start, stampa qualcosa nel terminale, vedremo uno ‘[0]’ seguito dal messaggio. Vedremo invece un ‘[1]’ seguito da un messaggio quando sarà JSON-server a stampare qualcosa. Potete per esempio notare il terzultimo rigo presente dell’immagine. In questo caso abbiamo effettuato una GET request visitando con il browser l’indirizzo http://localhost:3001/superheroes/hero_j1m8m4b.

JSON-server ci permetterà infatti di ottenere la lista completa di supereroi in formato JSON effettuando una GET request all’indirizzo http://localhost:3001/superheroes/.

output json server

Se vogliamo invece ottenere le informazioni di un singolo supereroe dovremo effettuare una GET Request all’indirizzo http://localhost:3001/superheroes/{id}. Per esempio possiamo effettuare una GET Request all’indirizzo http://localhost:3001/superheroes/hero_j1m8oq0c e otterremo il seguente risultato.

output json server get request

All’interno della nostra applicazione useremo la Fetch API per ottenere, aggiornare e modificare le risorse necessarie. Come potete vedere su caniuse.com, la maggior parte dei browser supporta questa funzionalità. In ogni caso CRA integra un polyfill (ovvero del codice che viene integrato in un progetto e che permette di utilizzare una funzione non supportata da un browser) per per la Fetch API.

Realizziamo i vari componenti

Prendiamo in considerazione l’immagine in alto nell’articolo e cominciamo a realizzare i diversi componenti della nostra applicazione. Apriamo il file App.js all’interno della directory src e digitiamo il seguente codice.

import React, { Component } from 'react';
import Form from './components/Form';
import List from './components/List';
import getId from './utils/generateId';
import {getAll, sendSuperheroToTheServer, deleteSuperheroFromTheServer} from './utils/network';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    
    this.state = {superheroes: [], error: ''};
    
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
    this.handleError = this.handleError.bind(this);
  }
  
  handleSubmit(dataObj) {
    if (dataObj.error === false) {
      const superhero = {id: getId(), name: dataObj.value.trim()};
      const superheroes = [...this.state.superheroes, superhero];
      this.setState({superheroes});
      sendSuperheroToTheServer(superhero)
        .catch(this.handleError);
    }
  }
  
  handleDelete(id) {
    let superheroes = this.state.superheroes;
    superheroes = superheroes.filter(superhero => superhero.id !== id);
    this.setState({superheroes});
    deleteSuperheroFromTheServer(id)
      .catch(this.handleError);
  }
  
  handleError(error) {
    this.setState({error: `Error: ${error.message}`}, () =>
       setTimeout(() => this.setState({error: ''}), 3000)
    );
  }
  
  componentDidMount() {
    getAll()
      .then(data => {
        this.setState({superheroes: data})
      })
      .catch(this.handleError);
  }
  
  render() {
    let message;
    if (this.state.error) {
      message = <span className="superhero-app__message__error">{this.state.error}</span>;
    }
    return (
      <div className="superhero-app">
        <div className="superhero-app__header">
          <h1>React Superheroes App</h1>
          <Form onSubmit={this.handleSubmit} />
          <p className="superhero-app__message">{message}</p>
        </div>
        <div className="superhero-app__content">
          <List onDelete={this.handleDelete} listItems={this.state.superheroes} />
        </div>
      </div>
    );
  }
}

export default App;

Il componente App.js è il componente che verrà inserito nel nodo radice della nostra applicazione eseguendo la funzione ReactDOM.render(). Il metodo render() del componente App restituisce un React Element all’interno del quale abbiamo inserito i due elementi <Form> e <List> che verranno creati utilizzando i componenti che abbiamo definito nei due file Form.js e List.js, importati in alto usando la sintassi di ES6. All’elemento <Form> passiamo il metodo handleSubmit() come prop mentre all’elemento <List> passiamo la funzione handleDelete() e un array di elementi che costituisce la lista dei supereroi. Ogni elemento dell’array ha una struttura del tipo {id: "", name: ""}.

Attraverso il metodo componentDidMount() (potete leggere l’articolo sul ciclo di vita di un componente per maggiori dettagli) inizializziamo lo stato del nostro componente con i dati che otteniamo dal server. In caso di errore durante questa fase, invochiamo la funzione handleError() che setta il valore di this.state.error con un messaggio di errore e dopo 3s resetta il valore di this.state.error. In questo modo verrà invocata nuovamente la funzione render() del componente App e verrà nascosto il messaggio di errore. Abbiamo usato la funzione getAll() da noi definita insieme alle altre due funzioni (sendSuperheroToTheServer, deleteSuperheroFromTheServer) all’interno del file network.js che si trova nella directory utils. (Vedremo a breve come abbiamo definito queste funzioni)

La funzione handleSubmit() si occuperà invece di inserire all’interno dell’array this.state.superheroes il nome del supereroe che viene digitato all’interno del campo di input e di salvare il nuovo valore anche sul server. Per essere precisi, dobbiamo evidenziare che non modifichiamo mai this.state.superheroes. React, infatti, ci suggerisce di non modificare l’oggetto State ma di crearne uno nuovo. Per far ciò, prendiamo l’array this.state.superheroes e concateniamo il nuovo valore (usiamo anche qui la sintassi di ES6 che in questo caso è equivalente a scrivere this.state.superheroes.concat(superhero)). Otteniamo così un nuovo array che passiamo alla funzione this.setState()(ancora una volta stiamo sfruttando le potenzialità di ES6. Scrivere {superheroes} è equivalente a scrivere {superheroes: superheroes})

La funzione handleDelete() viene invocata ogni volta che clicchiamo sul pulsante (la x rossa nell’immagine sopra) posto a fianco al nome di ogni supereroe. Alla funzione handleDelete() passiamo l’id dell’elemento che vogliamo eliminare. Attraverso la funzione Array.prototype.filter() rimuoviamo tale elemento (la funzione filter() restituisce un nuovo array) e aggiorniamo l’oggetto state così come i dati presenti sul server.

Apriamo ora il file utils/network.js.

const baseURL = "http://localhost:3001/superheroes";

function makeReq(url, errorMessage = '', options = {}) {
  return fetch(url, options)
    .then(response => {
      if (response.ok) {
        return response.json();
      } else {
        throw new Error(errorMessage);
      }
  });
}


export const getAll = () => {
  const errorMessage = 'Errore durante il download dei dati';
  return makeReq(baseURL, errorMessage);
};

export const sendSuperheroToTheServer = (superhero) => {
  const options = {
    method: 'POST',
    headers: {
      'Content-type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify(superhero)
  };
    
  const errorMessage = 'Errore nel collegamento col server';

  return makeReq(baseURL, errorMessage, options);
};

export const deleteSuperheroFromTheServer = (superheroId) => {
  const options = {
    method: 'DELETE',
    headers: {
      'Content-type': 'application/json',
      'Accept': 'application/json'
    }
  };

  const errorMessage = "Impossibile cancellare l'elemento dal server";

  const url = `${baseURL}/${superheroId}`;
    
  return makeReq(url, errorMessage, options);
}

All’interno di questo file ci limitiamo ad usare la fetch API (vi consiglio di leggere la documentazione). Effettuiamo delle richieste al server all’indirizzo http://localhost:3001/superheroes. La funzione fetch() restituisce una Promise che quando viene risolta ci fornisce la risposta del server. Nella funzione getAll() effettuiamo una GET request per ottenere tutti i dati salvati sul server lanciato con JSON-server (potete leggere la documentazione di JSON-server). Nella funzione sendSuperheroToTheServer() invece effettuiamo una POST request specificando il tipo di dati che stiamo inviando e il tipo di dati che vorremmo ricevere quando il server ci invierà una risposta. Inviamo quindi il nuovo dato superhero che avrà il solito formato {id: ”, name: ”}. Con la funzione deleteSuperheroFromTheServer() invece cancelliamo un supereroe dal server, effettuando una DELETE request all’indirizzo http://localhost:3001/superheroes/{superheroId}.

Nel file Form.js, all’interno della directory src/component, abbiamo invece creato il form. Al netto di alcune banali verifiche sul testo digitato, abbiamo usato un ‘Controlled Component’. Nel componente Form manteniamo alcune informazioni all’interno di un oggetto state per decidere se abilitare il pulsante ‘Aggiungi’ e applicare la classe ‘dirty’ al campo di input. Quando clicchiamo sul pulsante ‘Aggiungi’, invochiamo la funzione handleSubmit() che a sua volta chiama la funzione handleSubmit() del componente App passando come argomento un array con le proprietà error e value. Da notare che rispetto ai precedenti esempi, usiamo una versione aggiornata di React per cui usiamo la libreria ‘prop-type’ invece di React.PropTypes per specificare il tipo delle prop che vogliamo ricevere. Altro particolare da evidenziare è che abbiamo usato la sintassi ‘static PropTypes’ nella definizione del componente. Grazie a Babel è infatti possibile usare quella che viene definita "ES7 Property Initializer Syntax" che farà probabilmente parte delle future versioni di Javascript.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import './Form.css';

class Form extends Component {
  static propTypes = {
    onSubmit: PropTypes.func.isRequired
  }

  constructor(props) {
    super(props);
    this.pattern = /^[a-zA-Z0-9][a-zA-Z0-9'- ]+$/;
    this.emptyPattern = /^s*$/;
    this.state = {value: '',pristine: true, dirty: false, valid: false};
    this.handleSubmit = this.handleSubmit.bind(this);
    this.onBlur = this.onBlur.bind(this);
    this.onChange = this.onChange.bind(this);
    this.onFocus = this.onFocus.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();
    const value = this.state.value;
    if (this.pattern.test(this.state.value) === true) {
      this.props.onSubmit({value, error: false});
      this.setState({value: '', dirty: false, valid: false});
    } 
  }

  onChange(e) {
    const value = e.target.value;
    if (this.pattern.test(value) === true) {
      this.setState({dirty: true, valid: true, value});
    } else {
      this.setState({dirty: true, valid: false, value});
    }
  }

  onBlur(e) {
    const target = e.target;
    if (this.emptyPattern.test(target.value)) {
      target.value = '';
      this.setState({dirty: false});
    } else {
      this.setState({dirty: true});
    }
  }

  onFocus(e) {
    if (this.state.pristine) {
      this.setState({pristine: false});
    }
  }

  render() {
    let disabled = true;
    let classes = '';
    if (this.state.dirty) {
      classes='dirty';
    }
    if (this.state.valid) {
      disabled = false;
    }
    return (
      <form onSubmit={this.handleSubmit}>
        <input 
          className={classes}
          type="text"
          name="nome" 
          id="nome" 
          value={this.state.value}
          onChange={this.onChange}
          onBlur={this.onBlur}
          onFocus={this.onFocus}
        />
        <label htmlFor="nome">Aggiungi un nuovo supereroe</label>
        <span></span>
        <input type="submit" value="Aggiungi" disabled={disabled} />
      </form>
    );
  }
}

export default Form;

Nel file List.js abbiamo invece definito un componente List che mostra la lista dei vari supereroi. A ciascun elemento della lista passiamo la funzione this.props.onDelete ricevuta dal componente App.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ListItem from './ListItem';
import './List.css';

class List extends Component {
  static propTypes = {
    onDelete: PropTypes.func.isRequired,
    listItems: PropTypes.arrayOf(PropTypes.object).isRequired
  }

  render() {
    const fn = item => 
      <ListItem 
        key={item.id} 
        onDelete={this.props.onDelete}
        item={item} />;
    
    let listItems = this.props.listItems;
        
    if (listItems.length > 0) {
      listItems = listItems.map(fn);
    } else {
      listItems = <li className="list__empty-list">
          Lista vuota. <br /> Aggiungi un nuovo supereroe
        </li>;
    }

    return (
      <ul>
        {listItems}
      </ul>
    );
  }
}


export default List;

Infine, nel file Listitem.js creiamo il componente per mostrare il singolo elemento della lista che conterrà un nome e un pulsante per eliminare l’elemento stesso dalla lista. Quando clicchiamo sul pulsante verrà invocato il metodo handleClick che invocherà, tramite il componente List, il metodo handleDelete(id) del componente App. A questo metodo passeremo l’id dell’elemento su cui stiamo cliccando che è proprio quello che intendiamo eliminare.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import './ListItem.css';

class ListItem extends Component {
  static propTypes = {
    onDelete: PropTypes.func.isRequired,
    item: PropTypes.object.isRequired
  }
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.props.onDelete(this.props.item.id);
  }
  render() {
    const item = this.props.item;
    return (
      <li className="list__item"><span>{item.name}</span>
        <button onClick={this.handleClick}>×</button>
      </li>
    );
  }
}

export default ListItem;

Abbiamo così creato la nostra prima applicazione che comunica con un server per salvare i dati, cancellarli o aggiungerne dei nuovi. Come potete vedere, React mette a disposizione degli strumenti estremamente semplici da usare che in breve tempo ci permettono di realizzare un’applicazione. Nell’immagine sottostante è possibile vedere il risultato dell’applicazione creata.

risultato applicazione lista supereroi

Nel prossimo articolo vedremo brevemente come usare delle semplici animazioni all’interno delle applicazioni React.

Pubblicitร