In
React si possono usare varie librerie per l'accesso ad API remote, nell'esempio utilizzo la
Fetch API nata per sostituire l'oggetto
XMLHttpRequest presente negli attuali browser.
Per il codice base vedi
React come funziona.
In questo esempio visualizzo l'elenco dei comuni della lombardia, prendendo i dati da una API locale (
Cerca comuni italiani), questo è il codice completo:
function Waiting(props){
//lo stile nel codice è solo un esempio, meglio isolarlo in un file css/less/sass a parte
const myStyle = {
color: "#fff",
fontWeigth: "bold",
backgroundColor: "#080",
paddingLeft: "5px",
paddingRight: "5px",
marginLeft: "10px"
};
return <span>
{ props.show > 0 &&
<span style={myStyle}>Wait ...</span>
}
</span>
}
function Messages(props){
// quando elenco una serie di items il tag html deve avere
// la property "key" univoca
console.log("Messages: " + JSON.stringify(props));
return <ul>
{ props.messages.map( (item, index) => {
return <li key={index}><strong>{item}</strong> - <small>{new Date().toString() }</small></li>
})
}
</ul>
}
function List(props) {
const myStyle = {
height: "100px",
overflow: "auto",
border: "1px solid #ccc"
};
const rows = props.items.map( item =>{
//Each child in an array or iterator should have a unique "key" prop.
return <li key={item.id}>{item.comune} ( {item.cap} )</li>
});
return <div style={ myStyle }>
<ul>{ rows }</ul>
</div>
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
waiting: 0,
errors:[],
items: []
}
this.url = "comuni.json";
this.handleReload = this.handleReload.bind(this);
}
componentDidMount() {
//carico i dati all'inizio
this.loadItems();
}
handleReload() {
console.log("handleReload");
this.loadItems();
}
setWaiting(){
console.log("setWaiting: " + this.state.waiting);
this.setState( prevState => {
return {
waiting: prevState.waiting + 1,
errors: [],
items: []
}
});
}
loadingCompleted(data){
console.log("resetWaiting: " + this.state.waiting);
this.setState( prevState => {
return {
waiting: prevState.waiting > 0 ? prevState.waiting-1 : 0,
items: data
}
});
}
setError(ex){
console.log("setError: " + ex);
this.setState( prevState => {
const newErrors = [...prevState.errors, ex.message];
return {
waiting: prevState.waiting > 0 ? prevState.waiting-1 : 0,
errors: newErrors
}
});
}
loadItems(){
console.log("loadItems init");
//inizio caricamento
this.setWaiting();
console.log("loadItems waiting");
fetch(this.url)
.then( response => {
if(!response.ok){
throw Error("Network request failed");
}
return response.json();
})
.then( json => {
console.log("loadItems json");
const data = json.data.map( (item, index) => {
return {
id: index,
cap: item.cap,
comune: item.comune
}
});
this.loadingCompleted(data);
console.log("loadItems ok");
})
.catch( ex => {
console.log("loadItems catch:" + ex);
//fine caricamento
this.setError(ex);
});
}
render() {
console.log("E: " + JSON.stringify(this.state.errors));
return <div>
<button onClick={this.handleReload}>Reload</button>
<Waiting show={ this.state.waiting } />
<Messages messages={ this.state.errors } />
<List items={ this.state.items } />
</div>
}
}
//bootstrap
ReactDOM.render(
<App />,
document.getElementById("root")
);
e questo il risultato:
Per questo esempio come prima cosa ho creato un componente principale chiamato
App dove nel costruttore ho definito lo stato dell'applicazione:
constructor(props) {
super(props);
this.state = {
waiting: 0,
errors:[],
items: []
}
this.url = "comuni.json";
//this.url = "/api/app/comuni?q=LOM&f=codreg";
this.handleReload = this.handleReload.bind(this);
}
lo stato contiene 3 proprietà:
- waiting: conteggio delle chiamate asincrone in corso, se maggiore di 0 visualizza il messaggio Wait... ad indicare che è in corso un operazione asincrona (vedi componente Waiting)
- errors: array di messaggi di testo con gli errori generati dall'applicazione (vedi componente Messages)
- items: array di elementi da visualizzare, caricati tramite Fetch API (vedi componente List)
successivamente ho definito dei metodi per gestire lo stato di caricamento e gli errori:
- setWaiting: incrementa il contatore this.state.waiting
- loadingCompleted: decrementa il contatore this.state.waiting e aggiorna l'array this.state.items
- setError: imposta un messaggio di errore in this.state.errors
gli
items vengono caricati sul caricamento del componente tramite il metodo
componentDidMount che a sua volta chiama
this.loadItems. Questo metodo si occuopa di recuperare i dati dalla API tramite la
Fetch API:
//inizio la chiamata alle API
fetch(this.url)
.then( response => {
if(!response.ok){
//sollevo un eccezione se ho errori di comunicazione (vedi catch)
throw Error("Network request failed");
}
//se va tutto bene converto la risposta in json
return response.json();
})
.then( json => {
//ok, ho la risposta in json
//non uso direttamente l'oggetto come risposta,
//costruisco un altro oggetto con i dati nel formato che serve all'applicazione
//in questo modo mi svincolo dal formato della API, eventuali modifiche del formato della API andranno gestite solo qui
const data = json.data.map( (item, index) => {
return {
id: index,
cap: item.cap,
comune: item.comune
}
});
//notifico a React che i ho nuovi dati
this.loadingCompleted(data);
})
.catch( ex => {
//fine caricamento e impostazione errore
this.setError(ex);
});
map è una nuova funzione delle specifiche
ES6 che permette di eseguire, per ogni item di un array, una funzione di trasformazione e ritornare un nuovo array.
Come ultima azione nel componente
App gestisco il metodo
render:
render() {
return <div>
<button onClick={this.handleReload}>Reload</button>
<Waiting show={ this.state.waiting } />
<Messages messages={ this.state.errors } />
<List items={ this.state.items } />
</div>
}
Nell'applicazione vengono usati alcuni componenti. Il primo è
Waiting e si occupa di visualizzare il messaggio
Wait..., il suo scopo è indicare l'esecuzione di una o più chiamate asincrone:
function Waiting(props){
const myStyle = { .. vedi codice iniziale . };
return <span>
{ props.show > 0 &&
<span style={myStyle}>Wait ...</span>
}
</span>
}
Il componente si asppetta che venga passata una proprietà
show che rappresenta il numero di chiamate asincrone in corso (this.state.waiting).
L'altro componente, visualizza gli eventuali errori dell'applicazione ed è
Messages:
function Messages(props){
const myStyle = { ... };
return <ul style={myStyle}>
{ props.messages.map( (item, index) => {
return <li key={index}><strong>{item}</strong> - <small>{new Date().toString() }</small></li>
})
}
</ul>
}
Al componente ho dato un nome generico, in quanto con opportune modifiche potrebbe visualizzare anche messaggi informativi oltre a quelli di errore.
Si aspetta che venga passata nella proprietà
messages un array di messaggi stringa (this.state.errors).
Quando si cicla su una serie di elementi che vanno inseriti nel DOM,
React vuole che ogni elemento abbia un identificativo univoco, questo identificativo è rappresentato dalla proprietà
key.
L'ultimo componente è
List e si occupa di visualizzare gli elementi che sono stati memorizzati in
this.state.items e passati alla proprietà
items del componente:
function List(props) {
const myStyle = { ... };
const rows = props.items.map( item =>{
//Each child in an array or iterator should have a unique "key" prop.
return <li key={item.id}>{item.comune} ( {item.cap} )</li>
});
return <div style={ myStyle } className="elenco">
<ul>{ rows }</ul>
</div>
}
Faccio notare l'uso della proprietà
className in camelCase anziché la classica
class usata in
htmlPer questi ultimi 3 componenti, non avendo bisogno di gestire lo stato locale ad ogni componente, li ho stati creati tramite la keyword function anziché class.