Per recuperare dati in React, avvia la richiesta dentro useEffect, salva la risposta nello stato e mostra un messaggio di caricamento finché non arriva. Metti nell'array delle dipendenze i valori da cui dipende la richiesta, come un id, così l'effetto recupera di nuovo i dati quando cambiano.
Gli esempi di questa pagina non possono raggiungere la rete, quindi fetchUser è una finta API: una promise che si risolve dopo un ritardo, come fa fetch. Cambia 800 in 3000 per vedere il testo di caricamento restare più a lungo.
Con una vera API il corpo dell'effetto appare così:
useEffect(() => {
fetch('https://api.example.com/users/1')
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
Caricamento, errore e dati
Una richiesta ha tre esiti che lo schermo dovrebbe mostrare: ancora in caricamento, fallita, oppure completata. Tieni ciascuno nello stato, e azzerali quando l'effetto avvia una nuova richiesta.
Clicca User 3: la finta API rifiuta la promise, il messaggio di errore sostituisce il nome e la console mostra l'errore. Il blocco finally azzera loading in entrambi i casi, quindi una richiesta fallita non lascia mai lo spinner acceso.
Quando fetch parla con un server reale, una risposta 404 o 500 non rifiuta la promise. Controlla res.ok e genera tu l'errore:
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
async/await dentro un effetto
La funzione che passi a useEffect non può essere async. React si aspetta che restituisca nulla o una funzione di cleanup, e una funzione async restituisce sempre una promise.
// Wrong: the effect returns a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// Right: define an async function inside and call it
useEffect(() => {
async function load() {
const data = await fetchUser(id);
setUser(data);
}
load();
}, [id]);
Race condition
Quando l'id cambia rapidamente, due richieste sono in corso contemporaneamente. Nulla garantisce che rispondano in ordine. Se la più vecchia è più lenta, arriva per ultima e sovrascrive i dati dell'id che l'utente ha scelto più di recente.
La soluzione è un flag ignore. Ogni esecuzione dell'effetto ha il proprio flag, e la sua funzione di cleanup lo imposta a true. Poiché React esegue la funzione di cleanup prima dell'esecuzione successiva, una risposta di un'esecuzione superata vede ignore === true e viene scartata. Questo blocco renderizza lo stesso profilo due volte, senza e con il flag:
Clicca il pulsante e aspetta. La risposta veloce dello user 2 arriva per prima, quindi entrambi i profili la registrano e mostrano Grace. Circa un secondo e mezzo dopo il clic, arriva la risposta lenta dello user 1: il profilo senza flag passa ad Ada anche se è selezionato lo user 2, mentre quello con il flag registra dropped stale Ada e mantiene Grace.
Lo stesso bug compare in caselle di ricerca, schede e qualsiasi lista filtrata dal server. L'esempio sul caricamento qui sopra omette il flag per restare breve, ma ogni fetch in un effetto dovrebbe averlo, oppure usare l'abort qui sotto.
Annullare con AbortController
Il flag ignore scarta una risposta superata, ma la richiesta continua fino alla fine. Con il vero fetch puoi annullarla. Crea un AbortController nell'effetto, passa il suo signal a fetch e chiama abort() nella funzione di cleanup:
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // cancelled on purpose
setError(err);
}
}
load();
return () => controller.abort();
}, [id]);
Un fetch annullato rifiuta la promise con un AbortError, quindi il blocco catch lo salta invece di mostrarlo come un errore. L'abort copre anche lo smontaggio: quando il componente viene rimosso, la sua funzione di cleanup annulla la richiesta.
Spostarlo in un hook personalizzato
Caricamento, errore e protezione dalle race condition sono uguali in ogni componente che recupera dati. Mettili in un hook personalizzato e ogni componente chiede i dati in una riga.
fetchPosts è definita fuori dal componente, quindi la sua identità non cambia mai ed è sicura nell'array delle dipendenze. Definita dentro App, sarebbe una funzione nuova a ogni rendering, l'effetto verrebbe eseguito dopo ogni rendering e, poiché l'effetto imposta lo stato, non smetterebbe mai di recuperare dati.
Cambia 500 in 2000 e clicca il pulsante due volte di fila: la console mostra due richieste, e compaiono solo i post dell'argomento su cui ti sei fermato.
Recuperare dati in un gestore di eventi
Un effetto serve per i dati di cui il componente ha bisogno perché è sullo schermo: una pagina profilo carica il suo profilo. Quando una richiesta avviene perché l'utente ha fatto qualcosa, come premere Cerca o Salva, falla nel gestore di eventi. Sai esattamente cosa l'ha attivata, e nulla viene rieseguito quando cambia uno stato non correlato.
async function handleSubmit(e) {
e.preventDefault();
setStatus('saving');
const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
setStatus(res.ok ? 'saved' : 'error');
}
Errori comuni
Dipendenze mancanti. Un effetto che legge id ma ha [] recupera per sempre il primo utente. Elenca ogni valore usato dalla richiesta.
Recuperare dati nel corpo del componente. Un fetch fuori da un effetto viene eseguito a ogni rendering, e se imposta lo stato avvia un ciclo.
Fidarsi che fetch rifiuti in caso di errori. Rifiuta solo quando fallisce la rete. Controlla res.ok.
Dimenticare di azzerare il caricamento. Quando l'id cambia, reimposta loading a true, altrimenti i vecchi dati restano sullo schermo senza alcun segno che stanno arrivando quelli nuovi.
Quando usare una libreria o un framework
Recuperare dati in un effetto va bene per poche richieste. Non fa caching: apri lo stesso profilo due volte e lo recupera due volte. Non condivide i dati tra componenti, non riprova in caso di errore e non ricarica quando la scheda torna in primo piano. Le librerie lo fanno per te:
import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, error, isPending } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <p>{data.name}</p>;
}
TanStack Query e SWR sono le scelte più comuni per i dati lato client. I framework vanno oltre e caricano i dati sul server prima che la pagina arrivi al browser: Next.js con i server component, React Router con i loader. Questo evita il lampo di caricamento e la catena di richieste che si crea quando i dati di un genitore devono arrivare prima che un figlio possa anche solo iniziare a recuperare i suoi.
React 19: use() con Suspense
React 19 aggiunge use, che legge una promise durante il rendering. Il componente si sospende finché la promise non si risolve, e il confine <Suspense> più vicino mostra nel frattempo un fallback, quindi il componente stesso non ha alcuno stato di caricamento:
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>
La promise deve essere creata fuori dal componente (da un framework, una cache o un genitore), non dentro il rendering che la legge, altrimenti ogni rendering avvia una nuova richiesta. La pagina sull'hook use lo tratta con esempi eseguibili.
Domande frequenti
Come recupero i dati quando un componente React viene caricato?
Avvia la richiesta in un useEffect con i valori da cui dipende nell'array delle dipendenze ([] se non ce ne sono), e salva il risultato nello stato con useState. Mostra un messaggio di caricamento finché i dati non arrivano.
Perché la callback di useEffect non può essere async?
Una funzione async restituisce sempre una promise, e React si aspetta che l'effetto restituisca nulla oppure una funzione di cleanup. Scrivi una funzione async dentro l'effetto e chiamala subito.
Cos'è una race condition nel recupero dei dati in React?
Quando l'input cambia rapidamente, due richieste sono in corso e la più vecchia può rispondere per ultima, sovrascrivendo i dati più nuovi. Imposta un flag ignore nella funzione di cleanup dell'effetto e salta setState quando è attivo, oppure annulla la richiesta con un AbortController.
Devo usare useEffect o una libreria per recuperare dati?
useEffect funziona per app piccole e vale la pena capirlo. Per caching, deduplicazione, nuovi tentativi e ricaricamento dei dati, una libreria come TanStack Query o SWR, o il caricamento dei dati integrato in un framework come Next.js, ti evita di scrivere quella logica da te.
Come mostro uno spinner mentre i dati si caricano?
Tieni nello stato un booleano loading (o una stringa status), impostalo prima che la richiesta parta e azzeralo quando la richiesta termina, sia in caso di successo sia in caso di errore. Mostra lo spinner finché è vero.