Lavorare con le API REST
Collega DevNotes a un server reale: fetch, async/await, gestione degli stati di caricamento ed errore, custom hook useFetch.
fetch e il modello a Promise di JavaScriptAbortController per test sicuriIn questa lezione rendiamo DevNotes capace di parlare con il mondo esterno. Inizieremo consumando l’API pubblica JSONPlaceholder per simulare il caricamento delle note da un server, poi costruiremo il custom hook useFetch che useremo in tutta l’app. Nella prossima lezione scriveremo noi stessi il backend — per ora ci concentriamo sul lato React.
Perché i dati vengono da un server
Fino alla lezione 5, le note di DevNotes vivevano solo in useState: ogni volta che ricaricavi il browser, tutto spariva. Nelle applicazioni reali i dati devono persistere e spesso vengono prodotti o modificati da più utenti contemporaneamente. La soluzione è delegare la persistenza a un server e comunicare con lui tramite una API REST.
REST (Representational State Transfer) è uno stile architetturale per API HTTP. Ogni risorsa ha un URL univoco; le operazioni si esprimono con i metodi HTTP (GET, POST, PUT, DELETE); il server risponde con JSON. Il client non deve sapere nulla dello stato interno del server tra una richiesta e l’altra (stateless).
| Metodo HTTP | Operazione CRUD | Esempio su /notes |
|---|---|---|
GET | Read — leggi | GET /notes → lista note; GET /notes/3 → nota con id 3 |
POST | Create — crea | POST /notes con body JSON → nuova nota |
PUT | Update — sostituisci | PUT /notes/3 con body JSON → sostituisce nota 3 |
PATCH | Update — modifica parziale | PATCH /notes/3 con {"title":"..."} → aggiorna solo il titolo |
DELETE | Delete — elimina | DELETE /notes/3 → rimuove nota 3 |
Promise e async/await — il modello asincrono di JS
Una chiamata HTTP non è istantanea. JavaScript è single-threaded: non può bloccarsi ad aspettare la risposta del server. Il meccanismo che usa sono le Promise.
Una Promise è un oggetto che rappresenta il risultato futuro di un’operazione asincrona. Può trovarsi in tre stati: pending (in attesa), fulfilled (completata con successo) o rejected (fallita).
Ci sono due sintassi per lavorarci. La prima usa la catena di metodi:
// Sintassi .then() / .catch() — più verbosa fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
La seconda, preferita nei componenti React, usa la sintassi async/await:
// Sintassi async/await — leggibile come codice sincrono async function caricaDato() { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data); } catch (error) { console.error('Errore:', error.message); } }
fetch rigetta la Promise solo in caso di errore di rete (nessuna connessione, DNS fallito…). Se il server risponde con un codice HTTP come 404 o 500, la Promise viene comunque fulfilled. Devi controllare esplicitamente response.ok.
fetch() in un componente React — il primo tentativo
Il posto naturale dove eseguire una chiamata al caricamento del componente è useEffect. Vediamo la struttura base:
import { useState, useEffect } from 'react'; function NoteList() { const [notes, setNotes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchNotes = async () => { try { setLoading(true); const res = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!res.ok) throw new Error(`Errore ${res.status}`); const data = await res.json(); setNotes(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchNotes(); }, []); // array vuoto = esegui solo al mount if (loading) return <p>Caricamento...</p>; if (error) return <p>Errore: {error}</p>; return ( <ul> {notes.map(note => ( <li key={note.id}>{note.title}</li> ))} </ul> ); }
Funziona — ma se questo pattern viene ripetuto in cinque componenti diversi, hai duplicato la stessa logica cinque volte. Soluzione: estrarlo in un custom hook.
Custom hook useFetch
Un custom hook è semplicemente una funzione JavaScript il cui nome inizia con use e che può chiamare altri hook al suo interno. Non è un componente: non renderizza nulla, si limita a incapsulare logica riutilizzabile.
// src/hooks/useFetch.js import { useState, useEffect } from 'react'; export function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!url) return; // AbortController per cancellare la chiamata se il componente viene smontato const controller = new AbortController(); const fetchData = async () => { setLoading(true); setError(null); try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); setData(json); } catch (err) { if (err.name !== 'AbortError') setError(err.message); } finally { setLoading(false); } }; fetchData(); // cleanup: cancella la richiesta se l'url cambia o il componente si smonta return () => controller.abort(); }, [url]); // riesegui ogni volta che cambia url return { data, loading, error }; }
Ora qualsiasi componente che ha bisogno di dati può usarlo con una sola riga:
const { data: notes, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
AbortController — perché è importante
Immagina che l’utente navighi rapidamente da una pagina a un’altra. Il componente viene smontato prima che la risposta arrivi. Senza AbortController, quando la risposta arriva React prova comunque ad aggiornare lo stato di un componente che non esiste più → memory leak e warning in console.
Il componente si smonta → la risposta arriva → React prova a fare setState su un componente smontato → warning + potenziale memory leak.
Il cleanup di useEffect chiama controller.abort() → la richiesta viene annullata → il blocco catch intercetta l’AbortError e lo ignora.
Chiamate POST — inviare dati al server
Per creare, aggiornare o eliminare risorse, dobbiamo usare metodi diversi da GET e includere un body JSON nella richiesta:
async function creaNota(nota) { const res = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(nota), }); if (!res.ok) throw new Error(`Errore ${res.status}`); return res.json(); // restituisce la risorsa creata (con il nuovo id) } // utilizzo nel componente const handleSubmit = async (formData) => { try { const nuovaNota = await creaNota(formData); setNotes(prev => [...prev, nuovaNota]); } catch (err) { setError(err.message); } };
JSONPlaceholder — il tuo server di test
JSONPlaceholder è un’API pubblica gratuita perfetta per testare il frontend prima che il backend sia pronto. Espone risorse fittizie come /posts, /users, /todos. Le chiamate di scrittura (POST/PUT/DELETE) sembrano funzionare ma non persistono dati reali.
| Endpoint | Risposta | Utilizzo in DevNotes |
|---|---|---|
GET /posts | Array di 100 post | Simula lista note |
GET /posts/:id | Singolo post | Dettaglio nota |
POST /posts | Post creato (id: 101) | Crea nota |
PUT /posts/:id | Post aggiornato | Modifica nota |
DELETE /posts/:id | {} | Elimina nota |
Gestione degli stati: loading, error, empty, data
Una UI ben progettata gestisce quattro stati distinti dopo una chiamata API. Non lasciare l’utente a fissare una pagina bianca:
Mostra uno skeleton o uno spinner. Non bloccare l’intera UI.
Mostra i dati ricevuti. Caso normale.
Mostra un messaggio chiaro e un pulsante Riprova.
L’array è vuoto. Guida l’utente alla prossima azione.
// pattern completo nel componente const { data: notes, loading, error } = useFetch(API_URL); if (loading) return <LoadingSpinner />; if (error) return <ErrorMessage message={error} />; if (!notes?.length) return <EmptyState />; return <NoteList notes={notes} />;
Struttura file consigliata per i servizi API
Con la crescita dell’app conviene centralizzare le URL e la logica di fetch in un file servizio separato, così il componente non sa nulla dell’endpoint concreto:
// src/services/notesApi.js const BASE_URL = 'https://jsonplaceholder.typicode.com'; export const notesApi = { getAll: () => fetch(`${BASE_URL}/posts`).then(r => r.json()), getById: (id) => fetch(`${BASE_URL}/posts/${id}`).then(r => r.json()), create: (data) => fetch(`${BASE_URL}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }).then(r => r.json()), delete: (id) => fetch(`${BASE_URL}/posts/${id}`, { method: 'DELETE' }), };
Nel lab integrerai useFetch in DevNotes: cartella hooks/ e services/, LoadingSpinner, ErrorMessage, pagina NoteDetail collegata all’API, esercizi avanzati su useReducer e refetch manuale.
- fetch è nativo nel browser e restituisce una Promise — usa sempre
response.okper rilevare errori HTTP. - async/await rende il codice asincrono leggibile come codice sincrono; usalo sempre in coppia con
try/catch. - Il pattern loading / data / error va sempre gestito esplicitamente — non lasciare l’UI in uno stato indefinito.
- Il custom hook useFetch incapsula fetch + useState + useEffect ed evita la duplicazione del codice.
- AbortController è essenziale nel cleanup di useEffect per evitare memory leak quando il componente si smonta.
- Il file servizio API centralizza URL e logica di fetch separandola dalla logica dei componenti.