Ogni volta che scrivi useEffect per fare una chiamata API, stai costruendo un castello di carte. Gestione loading, errori, cache, refetch manuale, paginazione — tutto a mano, con mille useState e un codice che si allunga come le bollette a fine mese. Lo vediamo ogni giorno nei progetti che arrivano a Meteora Web: app React meravigliose a livello UI ma fragili nel recupero dati. React Query (ora TanStack Query) risolve tutto questo con poche righe di codice. Non è una libreria in più — è il modo giusto di gestire lo stato del server.
Cos'è React Query e perché non dovresti più scrivere useEffect per il fetching?
React Query è una libreria per la gestione dello stato asincrono lato server. Sembra una definizione pomposa, ma nella pratica significa una cosa sola: non devi più occuparti di cache, loading, errori e sincronizzazione. Te ne occupa la libreria, e lo fa meglio di come potresti fare tu a mano.
Il problema con useEffect + fetch: ogni volta che un componente monta, parti da zero. Il dato che hai già caricato un minuto fa? Lo ricarichi. L'utente torna su una pagina? Altra richiesta. Più componenti sullo stesso endpoint? Duplicazione. Tutto questo si traduce in server stressato, esperienza utente lenta e codice gonfio.
La soluzione React Query: un unico punto di accesso per ogni chiave di query. Cache intelligente, refetch automatico in background, stale-while-revalidate, gestione errori globale, paginazione nativa. E — cosa fondamentale — separazione netta tra stato del server e stato del client. I dati che arrivano dal backend vivono in React Query; quelli locali (filtri, modali, form) restano in useState o useReducer. Non si mescolano più.
Esempio pratico — la differenza la vedi subito:
// Il vecchio modo: useEffect + useState
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => { setUser(data); setLoading(false); })
.catch(err => { setError(err); setLoading(false); });
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{user.name}</div>;
}
// Con React Query
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{user.name}</div>;
}
Quindici righe diventate otto. E in più hai cache automatica, refetch quando la finestra torna in primo piano, retry su errore, e una gestione dello stato sincrona con il server. Sembra magia, ma è solo ingegneria fatta bene.
Sponsored Protocol
Come iniziare subito: Installa la libreria con npm i @tanstack/react-query, avvolgi la tua app con QueryClientProvider e inizia a sostituire i tuoi useEffect con useQuery. Nel 90% dei casi funziona al primo colpo.
Come funziona la cache di React Query e perché ti fa risparmiare chiamate al server?
React Query non si limita a memorizzare i risultati: implementa un modello di cache a livelli che decide quando mostrare il dato vecchio e quando richiederne uno nuovo. I concetti chiave sono stale e gcTime (garbage collection time) — prima chiamato cacheTime.
- staleTime: il tempo durante il quale il dato è considerato fresco. Se è fresco, React Query restituisce la cache senza fare alcuna richiesta di rete. Di default è 0, quindi ogni nuova sottoscrizione richiede un refetch. Impostando un valore (es. 30 secondi per dati semi-stabili) riduci le chiamate al server in modo drammatico.
- gcTime: quanto tempo tenere i dati in memoria dopo che nessun componente li sta più osservando. Di default 5 minuti. Se l'utente naviga via e torna entro quel tempo, il dato è ancora disponibile — nessuna richiesta.
Esempio concreto da un progetto reale: un e-commerce che seguiamo ha un catalogo prodotti che cambia poche volte al giorno. Abbiamo impostato staleTime a 5 minuti. Il risultato? L'80% delle visite allo shop ha zero richieste al backend per i prodotti. Il server respira, l'utente vede le pagine istantaneamente perché il dato è già caldo.
// Configurazione personalizzata per un catalogo prodotti
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minuti
gcTime: 10 * 60 * 1000 // 10 minuti in cache dopo l'ultimo uso
});
Cosa fare adesso: Analizza i tuoi dati. Quanto spesso cambiano? Ecco una regola pratica:
Sponsored Protocol
- Dati quasi statici (categorie, liste paesi, impostazioni): staleTime = Infinity, gcTime = Infinity (o molto alto).
- Dati semi-stabili (prodotti, articoli blog): staleTime = 30s-5min.
- Dati in tempo reale (notifiche, messaggi): staleTime = 0 (default).
Come gestire mutazioni (POST, PUT, DELETE) con React Query senza perdere la cache?
La parte difficile arriva quando scrivi dati. Un form che invia un nuovo ordine, una modifica profilo, una cancellazione. Con useEffect+fetch l'approccio standard è: fai la chiamata, poi ricarichi tutto. Con React Query usi useMutation e aggiorni la cache in modo ottimistico o invalidando le query collegate.
Invalidazione automatica delle query
Dopo una mutazione, puoi dire a React Query: “i dati di questa chiave non sono più validi, ricaricali”. In questo modo il prossimo componente che usa quella query farà un refetch in background.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddProductForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newProduct) => fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct)
}),
onSuccess: () => {
// Invalida la lista prodotti per forzare un refetch
queryClient.invalidateQueries({ queryKey: ['products'] });
}
});
return <form onSubmit={/* chiama mutation.mutate */}>...</form>;
}
Aggiornamento ottimistico: quando la UX è critica (like, follow, commenti), puoi aggiornare la cache subito prima che la risposta del server arrivi. Se la richiesta fallisce, React Query ripristina lo stato precedente.
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Annulla tutte le query in corso per 'todos'
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Salva lo snapshot corrente per eventuale rollback
const previousTodos = queryClient.getQueryData(['todos']);
// Aggiorna ottimisticamente
queryClient.setQueryData(['todos'], old => [...old, newTodo]);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback allo snapshot
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Alla fine (successo o errore) invalida per sincronizzare con server
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
Attenzione: l'aggiornamento ottimistico va usato con cautela. Funziona benissimo per azioni reversibili e con API affidabili. Per ordini pagamenti, meglio invalidare e mostrare uno stato loading temporaneo.
Sponsored Protocol
Cosa fare adesso: Per ogni mutazione nel tuo progetto, chiediti: “Serve aggiornamento immediato o posso aspettare il refetch?”. Se la risposta è “immediato”, implementa l'aggiornamento ottimistico. Altrimenti, una semplice invalidazione basta e avanza.
Quali sono le strategie avanzate di prefetching e query di paginazione?
React Query non è solo per semplici GET. Gestisce paginazione, infinite scroll e prefetching nativo. Se hai una lista paginata (es. ordini cliente, prodotti filtrati), usare useQuery con una chiave che include il numero di pagina è immediato. Ma la vera potenza arriva con useInfiniteQuery per scroll infinito.
Paginazione classica
function ProductList({ page }) {
const { data, isLoading, isPreviousData } = useQuery({
queryKey: ['products', page],
queryFn: () => fetch(`/api/products?page=${page}`).then(res => res.json()),
keepPreviousData: true, // Mostra i dati della pagina precedente mentre carica la nuova
});
// ... render con pulsanti 'Precedente'/'Successivo'
}
Prefetching: puoi precaricare la pagina successiva mentre l'utente sta ancora leggendo la corrente, abbattendo il tempo di attesa a zero.
// Nel componente che visualizza la lista
const queryClient = useQueryClient();
// Quando l'utente passa sopra il pulsante 'Successivo'
const prefetchNextPage = () => {
queryClient.prefetchQuery({
queryKey: ['products', page + 1],
queryFn: () => fetch(`/api/products?page=${page + 1}`).then(res => res.json())
});
}
Infinite scroll con useInfiniteQuery
import { useInfiniteQuery } from '@tanstack/react-query';
function InfiniteComments() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['comments'],
queryFn: ({ pageParam = 1 }) => fetch(`/api/comments?page=${pageParam}`).then(res => res.json()),
getNextPageParam: (lastPage, allPages) => {
// Ritorna undefined se non ci sono più pagine
return lastPage.nextPage ?? undefined;
}
});
// Unisci tutte le pagine
const comments = data?.pages.flatMap(page => page.items) || [];
return (
<div>
{comments.map(comment => <CommentItem key={comment.id} {...comment} />)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Caricamento...' : 'Carica altri'}
</button>
)}
</div>
);
}
Cosa fare adesso: Se il tuo progetto ha liste paginate, sostituisci il tuo sistema di paginazione manuale con useQuery + prefetch. Per l'infinite scroll, passa a useInfiniteQuery: ti gestisce automaticamente il salvataggio delle pagine e il caricamento della successiva.
Sponsored Protocol
Come integrare React Query con altri strumenti (React Router, state management) senza conflitti?
Una delle domande che ci fanno più spesso in Meteora Web: “React Query sostituisce Redux? Si può usare insieme a Zustand?”. La risposta è nessun conflitto, anzi: React Query si integra perfettamente con qualsiasi libreria di state management, perché gestisce un dominio diverso — i dati del server. Il segreto è tenere separati i due tipi di stato.
- Server state (dati da API): solo React Query. Non duplicare mai in Redux o Zustand.
- Client state (UI, form, filtri, modali): usa useState, useReducer, Zustand, Jotai, Pinia (per Vue, ma il principio è lo stesso).
Esempio pratico: un filtro in un campo input seleziona una categoria. La categoria è client state (useState), ma i prodotti filtrati sono server state (useQuery con queryKey che include la categoria come parametro).
function ShopPage() {
const [category, setCategory] = useState('all'); // client state
const { data: products } = useQuery({
queryKey: ['products', category], // server state
queryFn: () => fetch(`/api/products?category=${category}`).then(res => res.json())
});
return (
<div>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">Tutti</option>
<option value="electronics">Elettronica</option>
</select>
{products?.map(p => <ProductCard key={p.id} {...p} />)}
</div>
);
}
Con React Router: se i parametri della query vengono dall'URL (es. /shop?category=electronics), usa useSearchParams di React Router per leggere e scrivere i parametri, e passali come parte del queryKey. React Query reagirà automaticamente al cambio di param.
Sponsored Protocol
Cosa fare adesso: Rivedi il tuo progetto. Se hai dati duplicati tra React Query e uno store, elimina la copia. Se hai componenti che leggono dal server via useEffect e scrivono in Redux, sposta tutto su useQuery. Vedrai il codice dimezzarsi.
Quali sono gli errori comuni con React Query e come evitarli?
Dopo aver visto decine di implementazioni, ecco gli errori che ripetono più spesso:
- QueryKey non abbastanza specifica: se usi
['user']per un utente specifico, due pagine con ID diversi otterranno la stessa cache. Risultato: dati sbagliati. Rimedio: ogni parametro che influenza il risultato va nella queryKey. - Dimenticare di invalidare dopo una mutazione: l'utente aggiunge un prodotto, ma la lista non si aggiorna. Rimedio: chiama
queryClient.invalidateQueriesnelonSuccessdella mutation. - StaleTime = Infinity senza gcTime adeguato: se i dati non diventano mai stale, ma la cache viene pulita dopo 5 minuti, l'utente vedrà un caricamento a sorpresa. Rimedio: se usi staleTime alto, alza anche gcTime.
- Non gestire il refetch in background con indicatori visivi: React Query mostra i dati vecchi mentre ne carica di nuovi, ma senza un
isFetchingl'utente non sa che sta aggiornando. Rimedio: usaisFetchingper mostrare uno spinner di aggiornamento (non un full-page loading, ma un piccolo indicatore). - Mischiare mutazioni con query senza separarle: creare troppi hook custom che combinano lettura e scrittura può portare a confusione. Rimedio: mantieni separati useQuery per lettura e useMutation per scrittura. Solo in casi molto semplici unisci.
Cosa fare adesso
- Installa React Query nel tuo progetto:
npm install @tanstack/react-query - Configura il provider nel punto più alto dell'albero dei componenti (App.tsx o Layout).
- Sostituisci un useEffect/fetch con useQuery. Inizia da un endpoint semplice — prodotti, utenti, articoli.
- Aggiungi una mutazione per un form e invalida la query collegata.
- Rivedi staleTime e gcTime in base alla frequenza di aggiornamento dei tuoi dati.
- Leggi la documentazione ufficiale su TanStack Query per approfondire paginazione e query infinite — il tempo investito ti ripaga in ore di debug risparmiate.