TanStack Query i React — caching, polling og optimistiske opdateringer
TanStack Query (React Query) til datahentning: queryClient, useQuery, useMutation, polling, stale-while-revalidate, optimistiske opdateringer og invalidering.
Af M-Bus Gateway
M-Bus Gateway portalen bruger TanStack Query til al server state — ingen Redux, ingen manuel fetch-logik. Her er mønstrene der giver hurtig og konsistent UI.
QueryClient: Global konfiguration
// ui/src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Cached data er "frisk" i 60 sekunder — ingen refetch i det vindue:
staleTime: 60 * 1000,
// Behold cache i 5 minutter efter komponent unmounts:
gcTime: 5 * 60 * 1000,
// Retry 3× ved fejl med eksponentiel backoff:
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
// Refetch ved vinduefokus (bruger er væk og vender tilbage):
refetchOnWindowFocus: true,
},
mutations: {
retry: 0, // Mutationer retry aldrig automatisk
},
},
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
useQuery: Hent data med automatisk caching
// ui/src/pages/portal/Properties.tsx
import { useQuery } from "@tanstack/react-query";
import { portalProperties } from "@/api";
// Query key: Array — bruges til cache-identifikation + invalidering:
const PROPERTIES_KEY = ["properties"] as const;
export function PropertiesPage() {
const { data, isLoading, isError, error } = useQuery({
queryKey: PROPERTIES_KEY,
queryFn: () => portalProperties.list(),
// Override global staleTime for denne query:
staleTime: 30 * 1000,
});
if (isLoading) return <Spinner />;
if (isError) return <ErrorBanner message={error.message} />;
return (
<div>
{data.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
);
}
// Parameteriseret query — nyt cache-entry pr. propertyId:
function usePropertyDetail(propertyId: string) {
return useQuery({
queryKey: ["properties", propertyId],
queryFn: () => portalProperties.get(propertyId),
enabled: !!propertyId, // Kør kun hvis propertyId er defineret
});
}
Polling: Live gateway-status
// Gateway-status refreshes hvert 30 sek — gateway sender heartbeat hvert 5 min:
function useGatewayStatus(gatewayId: string) {
return useQuery({
queryKey: ["gateways", gatewayId, "status"],
queryFn: () => portalGateways.getStatus(gatewayId),
// Poll hvert 30 sekunder mens komponenten er mounted:
refetchInterval: 30 * 1000,
// Stop polling når vindue ikke er i fokus:
refetchIntervalInBackground: false,
});
}
// Conditional polling: Kun poll mens gateway er offline:
function useGatewayStatusUntilOnline(gatewayId: string) {
return useQuery({
queryKey: ["gateways", gatewayId, "status"],
queryFn: () => portalGateways.getStatus(gatewayId),
refetchInterval: (query) => {
// Stop polling når gateway er online:
if (query.state.data?.online) return false;
return 10 * 1000; // Poll hvert 10 sek ellers
},
});
}
useMutation + automatisk invalidering
// Opret ejendom og invalidér properties-listen automatisk:
import { useMutation, useQueryClient } from "@tanstack/react-query";
function useCreateProperty() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: PropertyIn) => portalProperties.create(data),
onSuccess: (newProperty) => {
// Invalidér listen → næste rendering refetches fra server:
queryClient.invalidateQueries({ queryKey: ["properties"] });
// Eller: Indsæt direkte i cache uden ekstra request:
queryClient.setQueryData(
["properties", newProperty.id],
newProperty,
);
},
onError: (error) => {
toast.error(`Oprettelse fejlede: ${error.message}`);
},
});
}
// I komponent:
function CreatePropertyForm() {
const createMutation = useCreateProperty();
const handleSubmit = (data: PropertyIn) => {
createMutation.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
{/* ... */}
<button disabled={createMutation.isPending}>
{createMutation.isPending ? "Opretter..." : "Opret ejendom"}
</button>
</form>
);
}
Optimistiske opdateringer
// Opdatér UI øjeblikkeligt — rull tilbage ved fejl:
function useDeleteMeter() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (meterId: string) => portalMeters.delete(meterId),
onMutate: async (meterId) => {
// Annullér igangværende fetches (undgå race condition):
await queryClient.cancelQueries({ queryKey: ["meters"] });
// Gem nuværende state til rollback:
const previousMeters = queryClient.getQueryData<Meter[]>(["meters"]);
// Optimistisk update — fjern fra cache øjeblikkeligt:
queryClient.setQueryData<Meter[]>(["meters"], (old) =>
old?.filter((m) => m.id !== meterId) ?? []
);
return { previousMeters };
},
onError: (_err, _meterId, context) => {
// Rollback til forrige state ved fejl:
if (context?.previousMeters) {
queryClient.setQueryData(["meters"], context.previousMeters);
}
toast.error("Sletning fejlede — prøv igen");
},
onSettled: () => {
// Altid invalidér efter mutation (success eller fejl):
queryClient.invalidateQueries({ queryKey: ["meters"] });
},
});
}
Infinite queries: Pagineret liste
// Aflæsningshistorik — load more ved scroll:
import { useInfiniteQuery } from "@tanstack/react-query";
function useReadingsInfinite(installationId: string) {
return useInfiniteQuery({
queryKey: ["readings", installationId],
queryFn: ({ pageParam = 1 }) =>
portalReadings.list({ installationId, page: pageParam, pageSize: 50 }),
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.page + 1 : undefined,
initialPageParam: 1,
});
}
function ReadingsList({ installationId }: { installationId: string }) {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useReadingsInfinite(installationId);
return (
<div>
{data?.pages.flatMap((page) => page.items).map((reading) => (
<ReadingRow key={reading.id} reading={reading} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Henter..." : "Hent flere"}
</button>
)}
</div>
);
}
Konklusion
TanStack Query eliminerer behovet for Redux/Zustand til server state. staleTime styrer cache-friskhed — gcTime styrer hukommelsesrydning. Polling med refetchInterval giver live-opdateringer uden WebSocket-kompleksitet. Optimistiske opdateringer sikrer instant UI-respons med automatisk rollback ved fejl. Cache-invalidering via invalidateQueries holder data konsistent på tværs af sider.
Se TypeScript API-typer guide eller React PWA offline guide.