Skip to content

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/useRef necessario
  • βœ… Invalidazioni coordinate - Mutations aggiornano automaticamente le query correlate
  • βœ… Override flessibile - Configurazione personalizzabile per ogni hook

πŸš€ Quick Start ​

Uso Base: Query (GET) ​

typescript
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) ​

typescript
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:

typescript
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:

typescript
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:

typescript
// βœ… 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 ​

HookTipoDescrizione
useLogin()MutationLogin utente + invalidazione cache
useCheckAuth()QueryVerifica sessione attiva
useLogout()MutationLogout + pulizia cache
useCompaniesList()QueryLista aziende (cache 5min)
useSetActiveCompany()MutationImposta azienda attiva
useUserSettings()QueryImpostazioni utente
useConfirmLoginData()QueryDati per confirm-login

Warehouse Module ​

HookTipoDescrizione
useWarehouseMovementsSummary(filters)QueryMovimenti warehouse con filtri dinamici

🎨 Pattern Avanzati ​

Query Condizionale ​

typescript
const { data } = useCompaniesList({
    enabled: isAuthenticated,  // Query attiva solo se autenticato
});

Override Configurazione ​

typescript
const { data } = useCompaniesList({
    staleTime: 0,              // Forza refetch (no cache)
    gcTime: 60 * 60 * 1000,    // Cache 1 ora invece di 10 minuti
});

Query con Filtri Dinamici ​

typescript
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 ​

typescript
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:

typescript
// 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:

typescript
// 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 ​

typescript
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 ​

  1. Usa TanStack Query per TUTTE le API calls

    typescript
    const { data } = useCompaniesList();  // βœ…
  2. NO useRef per prevenire fetch in StrictMode

    typescript
    // βœ… TanStack Query de-duplica automaticamente
    const { data } = useCompaniesList();
  3. Import diretti (NO barrel files)

    typescript
    import { useLogin } from "@/hooks/api/auth/useAuthApi";  // βœ…
  4. Usa query keys per invalidazioni coordinate

    typescript
    queryClient.invalidateQueries({ queryKey: authQueryKeys.all });  // βœ…

❌ DON'T ​

  1. NON usare useRef per fetch

    typescript
    // ❌ Non necessario con TanStack Query
    const hasFetchedRef = useRef(false);
    useEffect(() => {
        if (!hasFetchedRef.current) {
            hasFetchedRef.current = true;
            execute();
        }
    }, []);
  2. NON usare barrel files

    typescript
    import { useLogin } from "@/hooks/api/auth";  // ❌ No index.ts

πŸ§ͺ Testing ​

Test con QueryClientProvider ​

typescript
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 # Helpers

2. Query Keys ​

typescript
// 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 ​

typescript
// 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) ​

typescript
// 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:

typescript
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:

typescript
// ❌ 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:

typescript
const { data } = useProducts({
    staleTime: 5 * 60 * 1000,  // 5 minuti
});

πŸ“– Risorse ​


Versione: TanStack Query v5.90.7 Ultima revisione: 2025-11-10

Documentazione Elerama Frontend