TanStack Query - Guida Completa β
π Panoramica β
TanStack Query v5 Γ¨ integrato come layer di cache e gestione stato server per tutte le chiamate API del progetto.
Caratteristiche:
- β Cache automatica (5 minuti default, configurabile)
- β StrictMode safe - De-duplicazione automatica delle richieste
- β
Zero boilerplate - Nessun
useEffect/useRefnecessario - β Invalidazioni coordinate - Mutations aggiornano automaticamente le query correlate
- β Override flessibile - Configurazione personalizzabile per ogni hook
π Quick Start β
Uso Base: Query (GET) β
import { useCompaniesList } from "@/hooks/api/auth/useAuthApi";
export default function Companies() {
// β
Fetch automatico, cache 5min, de-duplicazione automatica
const { data, isLoading, error, refetch } = useCompaniesList();
if (isLoading) return <div>Caricamento...</div>;
if (error) return <div>Errore: {error.message}</div>;
return (
<div>
{data?.data.map(company => (
<div key={company.id}>{company.name}</div>
))}
</div>
);
}Uso Base: Mutation (POST/PUT/DELETE) β
import { useLogin } from "@/hooks/api/auth/useAuthApi";
import { useNavigate } from "react-router";
export default function Login() {
const navigate = useNavigate();
const { mutate: loginUser, isPending, error } = useLogin();
const handleSubmit = (credentials: { username: string; password: string }) => {
loginUser(credentials, {
onSuccess: () => navigate("/companies"),
onError: (err) => console.error(err.message),
});
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={isPending}>
{isPending ? "Login..." : "Login"}
</button>
</form>
);
}βοΈ Configurazione β
QueryClient Setup β
In app/lib/query-client.ts:
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minuti
gcTime: 10 * 60 * 1000, // 10 minuti
retry: 1,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
},
mutations: {
retry: 0, // Mai ritentare mutations
},
},
});Provider in Root β
In app/root.tsx:
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/query-client";
export default function Root() {
return (
<QueryClientProvider client={queryClient}>
{/* App content */}
</QueryClientProvider>
);
}π¦ Organizzazione File β
Struttura Modulare β
app/hooks/api/
βββ useFetchJson.ts # Utility generica
β
βββ auth/ # π Modulo Auth
β βββ useAuthApi.ts # 7 hooks (useLogin, useCompaniesList, ...)
β βββ auth.query-keys.ts # Query keys factory
β βββ auth.query-helpers.ts # Invalidation helpers
β
βββ warehouse/ # π¦ Modulo Warehouse
βββ useWarehouseApi.ts # 1 hook (useWarehouseMovementsSummary)
βββ warehouse.query-keys.ts
βββ warehouse.query-helpers.tsβ οΈ Importante: NO barrel files (index.ts). Usa sempre import diretti:
// β
Corretto
import { useLogin } from "@/hooks/api/auth/useAuthApi";
import { authQueryKeys } from "@/hooks/api/auth/auth.query-keys";
// β Sbagliato
import { useLogin } from "@/hooks/api/auth"; // No index.tsπ Hooks Disponibili β
Auth Module β
| Hook | Tipo | Descrizione |
|---|---|---|
useLogin() | Mutation | Login utente + invalidazione cache |
useCheckAuth() | Query | Verifica sessione attiva |
useLogout() | Mutation | Logout + pulizia cache |
useCompaniesList() | Query | Lista aziende (cache 5min) |
useSetActiveCompany() | Mutation | Imposta azienda attiva |
useUserSettings() | Query | Impostazioni utente |
useConfirmLoginData() | Query | Dati per confirm-login |
Warehouse Module β
| Hook | Tipo | Descrizione |
|---|---|---|
useWarehouseMovementsSummary(filters) | Query | Movimenti warehouse con filtri dinamici |
π¨ Pattern Avanzati β
Query Condizionale β
const { data } = useCompaniesList({
enabled: isAuthenticated, // Query attiva solo se autenticato
});Override Configurazione β
const { data } = useCompaniesList({
staleTime: 0, // Forza refetch (no cache)
gcTime: 60 * 60 * 1000, // Cache 1 ora invece di 10 minuti
});Query con Filtri Dinamici β
const [filters, setFilters] = useState({ status: "active" });
const { data } = useWarehouseMovementsSummary(filters);
// Cambiare filtri trigghera automaticamente un refetch
const handleFilterChange = (newFilters) => {
setFilters(newFilters); // Query si aggiorna automaticamente
};Mutation con Callbacks β
const { mutate: setCompany } = useSetActiveCompany();
setCompany(
{ companyId: 123 },
{
onSuccess: (data) => {
console.log("Azienda impostata:", data);
navigate("/dashboard");
},
onError: (error) => {
console.error("Errore:", error.message);
toast.error("Impossibile cambiare azienda");
},
onSettled: () => {
// Eseguito sempre, sia su success che error
console.log("Operazione completata");
},
}
);π Query Keys e Invalidazioni β
Query Keys Factory β
I query keys sono organizzati gerarchicamente:
// app/hooks/api/auth/auth.query-keys.ts
export const authQueryKeys = {
all: ["auth"] as const,
session: () => [...authQueryKeys.all, "session"] as const,
companies: () => [...authQueryKeys.all, "companies"] as const,
userSettings: () => [...authQueryKeys.all, "userSettings"] as const,
};Invalidazione Automatica β
Le mutations invalidano automaticamente le query correlate:
// In useAuthApi.ts - Esempio useLogin
export function useLogin() {
return useMutation({
mutationFn: login,
onSuccess: () => {
// Invalida TUTTE le query auth
queryClient.invalidateQueries({ queryKey: authQueryKeys.all });
},
});
}Invalidazione Manuale β
import { queryClient } from "@/lib/query-client";
import { authQueryKeys } from "@/hooks/api/auth/auth.query-keys";
// Invalida query specifiche
queryClient.invalidateQueries({ queryKey: authQueryKeys.companies() });
// Invalida tutte le query auth
queryClient.invalidateQueries({ queryKey: authQueryKeys.all });π― Best Practices β
β DO β
Usa TanStack Query per TUTTE le API calls
typescriptconst { data } = useCompaniesList(); // βNO useRef per prevenire fetch in StrictMode
typescript// β TanStack Query de-duplica automaticamente const { data } = useCompaniesList();Import diretti (NO barrel files)
typescriptimport { useLogin } from "@/hooks/api/auth/useAuthApi"; // βUsa query keys per invalidazioni coordinate
typescriptqueryClient.invalidateQueries({ queryKey: authQueryKeys.all }); // β
β DON'T β
NON usare useRef per fetch
typescript// β Non necessario con TanStack Query const hasFetchedRef = useRef(false); useEffect(() => { if (!hasFetchedRef.current) { hasFetchedRef.current = true; execute(); } }, []);NON usare barrel files
typescriptimport { useLogin } from "@/hooks/api/auth"; // β No index.ts
π§ͺ Testing β
Test con QueryClientProvider β
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { useCompaniesList } from "@/hooks/api/auth/useAuthApi";
test("dovrebbe caricare le aziende", async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }, // Nessun retry nei test
},
});
const { result } = renderHook(() => useCompaniesList(), {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toBeDefined();
});π§ Creare Nuovi Moduli β
Quando aggiungi un nuovo modulo API (es. products):
1. Crea la struttura β
app/hooks/api/products/
βββ useProductsApi.ts # Hooks
βββ products.query-keys.ts # Query keys
βββ products.query-helpers.ts # Helpers2. Query Keys β
// products.query-keys.ts
export const productsQueryKeys = {
all: ["products"] as const,
lists: () => [...productsQueryKeys.all, "list"] as const,
list: (filters: any) => [...productsQueryKeys.lists(), filters] as const,
details: () => [...productsQueryKeys.all, "detail"] as const,
detail: (id: number) => [...productsQueryKeys.details(), id] as const,
};3. Hooks β
// useProductsApi.ts
import { useQuery, useMutation } from "@tanstack/react-query";
import { getProducts, createProduct } from "@/api/products/products.api";
import { productsQueryKeys } from "./products.query-keys";
import { queryClient } from "@/lib/query-client";
export function useProductsList(filters?: any, options?: any) {
return useQuery({
queryKey: productsQueryKeys.list(filters),
queryFn: () => getProducts(filters),
...options,
});
}
export function useCreateProduct() {
return useMutation({
mutationFn: createProduct,
onSuccess: () => {
// Invalida la lista prodotti
queryClient.invalidateQueries({
queryKey: productsQueryKeys.lists()
});
},
});
}4. Helpers (opzionale) β
// products.query-helpers.ts
import { queryClient } from "@/lib/query-client";
import { productsQueryKeys } from "./products.query-keys";
export function invalidateProductsQueries() {
return queryClient.invalidateQueries({
queryKey: productsQueryKeys.all
});
}π Troubleshooting β
Query non si aggiorna β
Problema: I dati non si aggiornano dopo una mutation.
Soluzione: Verifica che la mutation invalidi le query corrette:
export function useCreateProduct() {
return useMutation({
mutationFn: createProduct,
onSuccess: () => {
// β
Invalida la lista prodotti
queryClient.invalidateQueries({
queryKey: productsQueryKeys.lists()
});
},
});
}Query si esegue troppo spesso β
Problema: La query viene chiamata ad ogni render.
Soluzione: Verifica che i filtri siano stabili:
// β Crea un nuovo oggetto ad ogni render
const { data } = useProducts({ status: "active" });
// β
Usa useState per stabilitΓ
const [filters] = useState({ status: "active" });
const { data } = useProducts(filters);Cache non funziona β
Problema: I dati vengono sempre ri-fetchati.
Soluzione: Verifica staleTime nella configurazione:
const { data } = useProducts({
staleTime: 5 * 60 * 1000, // 5 minuti
});π Risorse β
- TanStack Query Docs - Documentazione ufficiale
- React Query DevTools - Debug tool (opzionale)
- Best Practices - Best practices del progetto
Versione: TanStack Query v5.90.7 Ultima revisione: 2025-11-10