Introduzione
TanStack Query (precedentemente React Query) è una libreria che semplifica l’invio di richieste HTTP e la sincronizzazione tra frontend e backend. Gestisce automaticamente caching, refetching, gestione degli stati di loading ed errori, riducendo il boilerplate necessario quando si usa useEffect e fetch. Questo articolo esplora come funziona TanStack Query, come configurare query e mutazioni, e come gestire cache e aggiornamenti ottimistici.
Perché TanStack Query
Con useEffect e fetch, si gestisce manualmente loading state, error state e data state. TanStack Query automatizza queste operazioni e aggiunge funzionalità avanzate come caching automatico, refetching intelligente e sincronizzazione dei dati.
Vantaggi
TanStack Query offre:
- Caching automatico: i dati vengono memorizzati e riutilizzati
- Refetching intelligente: aggiornamento automatico quando si torna alla pagina o si riacquista il focus
- Gestione stati: loading, error e data gestiti automaticamente
- Invalidazione query: possibilità di forzare il refetch quando i dati cambiano
- Optimistic updates: aggiornamento immediato dell’UI prima della risposta del server
Setup Base
Per utilizzare TanStack Query, si installa il package e si configura il QueryClientProvider:
npm install @tanstack/react-queryimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() { return ( <QueryClientProvider client={queryClient}> {/* Componenti dell'app */} </QueryClientProvider> );}QueryClient è l’oggetto di configurazione che gestisce cache e query. QueryClientProvider avvolge l’applicazione e fornisce il client a tutti i componenti.
useQuery: Data Fetching
useQuery è l’hook principale per fetchare dati. Accetta un oggetto di configurazione con queryFn e queryKey.
Configurazione Base
import { useQuery } from '@tanstack/react-query';
function EventsList() { const { data, isPending, isError, error } = useQuery({ queryKey: ['events'], queryFn: fetchEvents });
if (isPending) return <p>Loading...</p>; if (isError) return <p>Error: {error.message}</p>;
return ( <ul> {data.map(event => ( <li key={event.id}>{event.title}</li> ))} </ul> );}queryFn è la funzione che esegue la richiesta HTTP. Deve restituire una Promise. queryKey è un array che identifica univocamente la query e viene usato per il caching.
Query Function
La queryFn riceve automaticamente un oggetto con signal (per abort) e informazioni sulla query:
async function fetchEvents({ signal }) { const response = await fetch('http://localhost:8080/events', { signal // Permette di abortire la richiesta });
if (!response.ok) { throw new Error('Failed to fetch events'); }
return response.json();}Query Key Dinamiche
Le query key possono includere valori dinamici:
function EventDetails({ eventId }) { const { data } = useQuery({ queryKey: ['events', eventId], queryFn: ({ signal }) => fetchEvent({ signal, id: eventId }) });}Query con key diverse vengono trattate come query separate e hanno cache separate.
Parametri Dinamici nella Query Function
Per passare parametri dinamici alla query function, si può wrappare la funzione:
function SearchEvents({ searchTerm }) { const { data } = useQuery({ queryKey: ['events', { search: searchTerm }], queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }) });}Alternativamente, si può estrarre i parametri dalla query key usando il secondo elemento dell’array:
const { data } = useQuery({ queryKey: ['events', { search: searchTerm }], queryFn: ({ signal, queryKey }) => { const { search } = queryKey[1]; return fetchEvents({ signal, searchTerm: search }); }});Abilitare/Disabilitare Query
Per disabilitare una query fino a quando non si verifica una condizione:
const { data } = useQuery({ queryKey: ['events', searchTerm], queryFn: () => fetchEvents({ searchTerm }), enabled: searchTerm !== undefined});Se enabled è false, la query non viene eseguita. isPending sarà true se la query è disabilitata, mentre isLoading sarà false.
Caching e Stale Time
TanStack Query memorizza automaticamente i dati in cache. Quando una query viene rieseguita con la stessa key, i dati vengono mostrati immediatamente dalla cache mentre viene inviata una richiesta in background per aggiornare i dati.
staleTime
staleTime controlla dopo quanto tempo i dati vengono considerati “stale” e viene inviata una richiesta di aggiornamento:
const { data } = useQuery({ queryKey: ['events'], queryFn: fetchEvents, staleTime: 5000 // 5 secondi});Con staleTime: 5000, se la query viene rieseguita entro 5 secondi, non viene inviata una nuova richiesta. Il default è 0, quindi i dati vengono sempre considerati stale.
gcTime (Garbage Collection Time)
gcTime controlla per quanto tempo i dati vengono mantenuti in cache quando non sono più utilizzati:
const { data } = useQuery({ queryKey: ['events'], queryFn: fetchEvents, gcTime: 30000 // 30 secondi});Il default è 5 minuti. Dopo questo tempo, i dati vengono rimossi dalla cache.
useMutation: Mutazioni
useMutation gestisce richieste che modificano dati (POST, PUT, DELETE). A differenza di useQuery, le mutazioni non vengono eseguite automaticamente.
Configurazione Base
import { useMutation } from '@tanstack/react-query';
function NewEvent() { const { mutate, isPending, isError, error } = useMutation({ mutationFn: createEvent });
const handleSubmit = (formData) => { mutate({ event: formData }); };
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.target)); }}> {/* campi form */} <button disabled={isPending}> {isPending ? 'Creating...' : 'Create'} </button> </form> );}mutate è la funzione che triggera la mutazione. Viene chiamata manualmente, non automaticamente quando il componente viene renderizzato.
Callback delle Mutazioni
Le mutazioni supportano callback per gestire successo ed errori:
const { mutate } = useMutation({ mutationFn: createEvent, onSuccess: () => { // Eseguito quando la mutazione ha successo navigate('/events'); }, onError: (error) => { // Eseguito quando la mutazione fallisce console.error('Failed to create event:', error); }});Invalidazione Query
Dopo una mutazione, spesso si vuole invalidare le query correlate per forzare il refetch:
import { useQueryClient } from '@tanstack/react-query';
function NewEvent() { const queryClient = useQueryClient();
const { mutate } = useMutation({ mutationFn: createEvent, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['events'] }); navigate('/events'); } });}invalidateQueries marca le query come stale e forza il refetch se i componenti che le usano sono montati. Per evitare il refetch immediato:
queryClient.invalidateQueries({ queryKey: ['events'], refetchType: 'none' // Invalida ma non refetcha immediatamente});Optimistic Updates
Gli optimistic updates aggiornano l’UI immediatamente, prima della risposta del server, e fanno rollback se la mutazione fallisce.
Implementazione
const { mutate } = useMutation({ mutationFn: updateEvent, onMutate: async (newEventData) => { // Cancella query in corso per evitare conflitti await queryClient.cancelQueries({ queryKey: ['events', eventId] });
// Salva i dati precedenti per il rollback const previousEvent = queryClient.getQueryData([ 'events', eventId ]);
// Aggiorna ottimisticamente i dati queryClient.setQueryData( ['events', eventId], newEventData.event );
// Ritorna il contesto per il rollback return { previousEvent }; }, onError: (error, newEventData, context) => { // Rollback in caso di errore queryClient.setQueryData( ['events', eventId], context.previousEvent ); }, onSettled: () => { // Eseguito sempre, successo o errore queryClient.invalidateQueries({ queryKey: ['events', eventId] }); }});onMutate viene eseguito prima della mutazione. onError riceve il contesto da onMutate per fare rollback. onSettled viene sempre eseguito e può essere usato per invalidare le query.
Flusso degli optimistic updates
- L’utente triggera la mutazione
onMutateviene eseguito: si salvano i dati precedenti e si aggiorna la cache- L’UI si aggiorna immediatamente con i nuovi dati
- La richiesta HTTP viene inviata
- Se ha successo:
onSuccessviene eseguito (opzionale) - Se fallisce:
onErrorviene eseguito e si fa rollback ai dati precedenti onSettledviene sempre eseguito per sincronizzare con il backend
Integrazione con React Router
TanStack Query può essere combinato con React Router usando fetchQuery nei loader:
import { queryClient } from './util/http';
export async function loader({ params }) { await queryClient.fetchQuery({ queryKey: ['events', params.eventId], queryFn: ({ signal }) => fetchEvent({ signal, id: params.eventId }) });
return null; // React Router gestirà la navigazione}Nel componente, si continua a usare useQuery per beneficiare di refetching automatico e altre funzionalità. I dati vengono presi dalla cache se disponibili.
Per le mutazioni con React Router, si può usare un’action:
export async function action({ request, params }) { const formData = await request.formData(); const updatedData = Object.fromEntries(formData);
await updateEvent({ id: params.eventId, event: updatedData });
await queryClient.invalidateQueries({ queryKey: ['events'] });
return redirect(`/events/${params.eventId}`);}Nel componente, si usa useSubmit di React Router per triggerare l’action:
import { useSubmit, useNavigation } from 'react-router-dom';
function EditEvent() { const submit = useSubmit(); const navigation = useNavigation();
const handleSubmit = (formData) => { submit(formData, { method: 'PUT' }); };
const isSubmitting = navigation.state === 'submitting';
return ( <form onSubmit={handleSubmit}> {/* campi form */} <button disabled={isSubmitting}> {isSubmitting ? 'Updating...' : 'Update'} </button> </form> );}Evitare Richieste Ridondanti
Quando si usa React Router con TanStack Query, si può aumentare staleTime per evitare refetch immediati:
const { data } = useQuery({ queryKey: ['events', eventId], queryFn: ({ signal }) => fetchEvent({ signal, id: eventId }), staleTime: 10000 // 10 secondi});Hook Utili
useIsFetching
useIsFetching indica se TanStack Query sta fetchando dati da qualche parte nell’applicazione:
import { useIsFetching } from '@tanstack/react-query';
function Header() { const isFetching = useIsFetching();
return ( <header> {isFetching > 0 && <progress />} </header> );}Restituisce un numero: 0 se non ci sono fetch in corso, un numero maggiore se ci sono fetch attivi.
useQueryClient
useQueryClient restituisce l’istanza del QueryClient per interagire direttamente con la cache:
import { useQueryClient } from '@tanstack/react-query';
function MyComponent() { const queryClient = useQueryClient();
const handleRefresh = () => { queryClient.invalidateQueries({ queryKey: ['events'] }); };
// ...}Riepilogo
TanStack Query semplifica il data fetching e la gestione dello stato per le richieste HTTP. useQuery gestisce il fetching automatico con caching e refetching intelligente. useMutation gestisce le mutazioni con callback per successo ed errori.
Le query key identificano univocamente le query e vengono usate per il caching. staleTime controlla quando i dati vengono considerati obsoleti. gcTime controlla per quanto tempo i dati vengono mantenuti in cache.
L’invalidazione delle query forza il refetch quando i dati cambiano. Gli optimistic updates aggiornano l’UI immediatamente e fanno rollback se la mutazione fallisce.
TanStack Query può essere integrato con React Router usando fetchQuery nei loader e actions per le mutazioni. useIsFetching permette di mostrare indicatori globali di loading.