· React· TanStack Query· React Query· IoT· dashboard· TypeScript· FastAPI· caching· real-time
React + TanStack Query til IoT-dashboards — mønstre og caching
TanStack Query (React Query) til IoT-dashboards: smart caching, polling for live data, optimistic updates, infinite scroll og TypeScript-integration med FastAPI.
Af M-Bus Gateway
M-Bus Gateway portalen bruger TanStack Query til alle server-state operationer. Her er mønstrene der giver responsive dashboards med minimal API-belastning.
Hvorfor TanStack Query frem for useState + fetch?
// GAMMELT: Manuel fetch + useState
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/v1/gateways")
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// Problem: Ingen caching, ingen deduplikering, ingen fejlhåndtering,
// ingen background refresh, ingen staleness-håndtering
// MED TanStack Query:
const { data, isLoading, error } = useQuery({
queryKey: ["gateways"],
queryFn: () => api.gateways.list(),
staleTime: 30_000, // Data er "frisk" i 30 sekunder
refetchInterval: 60_000, // Auto-refresh hvert 60. sekund
});
// Alt det andet håndteres automatisk
API-klient med TypeScript
// ui/src/api.ts
const BASE_URL = "/api/v1";
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const token = localStorage.getItem("access_token");
const resp = await fetch(`${BASE_URL}${path}`, {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
...options,
});
if (!resp.ok) {
const error = await resp.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${resp.status}`);
}
return resp.json();
}
// Typede API-funktioner:
export const portalGateways = {
list: () => apiFetch<Gateway[]>("/gateways"),
get: (id: string) => apiFetch<GatewayDetail>(`/gateways/${id}`),
health: () => apiFetch<GatewayHealthSummary>("/gateways/health"),
};
export const portalReadings = {
latest: (installationId: string) =>
apiFetch<Reading>(`/readings/latest?installation_id=${installationId}`),
monthly: (unitId: string) =>
apiFetch<MonthlyReading[]>(`/readings/monthly?unit_id=${unitId}`),
};
Query Keys: Struktureret caching
// ui/src/queryKeys.ts
// Hierarkisk query key-struktur giver præcis cache-invalidering:
export const queryKeys = {
gateways: {
all: ["gateways"] as const,
list: () => [...queryKeys.gateways.all, "list"] as const,
detail: (id: string) => [...queryKeys.gateways.all, "detail", id] as const,
health: () => [...queryKeys.gateways.all, "health"] as const,
},
readings: {
all: ["readings"] as const,
latest: (installationId: string) =>
[...queryKeys.readings.all, "latest", installationId] as const,
monthly: (unitId: string) =>
[...queryKeys.readings.all, "monthly", unitId] as const,
},
settlements: {
all: ["settlements"] as const,
list: (propertyId?: string) =>
[...queryKeys.settlements.all, "list", propertyId] as const,
},
};
// Invalidér alle gateway-queries:
queryClient.invalidateQueries({ queryKey: queryKeys.gateways.all });
// Invalidér kun detail for specifik gateway:
queryClient.invalidateQueries({ queryKey: queryKeys.gateways.detail("GW-0001") });
Gateway dashboard med polling
// ui/src/pages/portal/GatewayDashboard.tsx
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "../queryKeys";
import { portalGateways } from "../api";
export function GatewayDashboard() {
// Health-data: Refresh hvert 30. sekund:
const { data: health, isLoading } = useQuery({
queryKey: queryKeys.gateways.health(),
queryFn: portalGateways.health,
refetchInterval: 30_000,
staleTime: 20_000,
});
// Gateway-liste: Refresh hvert 5. minut:
const { data: gateways } = useQuery({
queryKey: queryKeys.gateways.list(),
queryFn: portalGateways.list,
refetchInterval: 300_000,
staleTime: 240_000,
});
if (isLoading) return <div className="loading-skeleton" />;
return (
<div>
<div className="kpi-grid">
<KpiCard label="Online" value={health?.online_count} color="green" />
<KpiCard label="Offline" value={health?.offline_count} color="red" />
<KpiCard label="Stale data" value={health?.stale_count} color="amber" />
</div>
<GatewayTable gateways={gateways ?? []} />
</div>
);
}
Optimistic updates: Hurtig UI-respons
// Ved mutation: Opdatér UI straks, synkronisér med server bagefter:
import { useMutation, useQueryClient } from "@tanstack/react-query";
function GatewaySettings({ gatewayId }: { gatewayId: string }) {
const queryClient = useQueryClient();
const { mutate: sendCommand } = useMutation({
mutationFn: (cmd: string) =>
apiFetch(`/gateways/${gatewayId}/command`, {
method: "POST",
body: JSON.stringify({ command: cmd }),
}),
// Optimistic: Vis straks at kommando er sendt:
onMutate: async (cmd) => {
await queryClient.cancelQueries({
queryKey: queryKeys.gateways.detail(gatewayId),
});
const previous = queryClient.getQueryData(
queryKeys.gateways.detail(gatewayId)
);
queryClient.setQueryData(queryKeys.gateways.detail(gatewayId), (old: any) => ({
...old,
last_command: cmd,
last_command_at: new Date().toISOString(),
}));
return { previous };
},
// Rollback ved fejl:
onError: (err, cmd, context) => {
queryClient.setQueryData(
queryKeys.gateways.detail(gatewayId),
context?.previous
);
},
// Refresh efter success:
onSettled: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.gateways.detail(gatewayId),
});
},
});
return (
<button onClick={() => sendCommand("reboot")}>
Genstart gateway
</button>
);
}
Stale-while-revalidate: Aldrig tom skærm
// Konfiguration der balancerer friskhed og API-kald:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Vis stale data mens ny data hentes:
staleTime: 60_000, // 1 minut = "frisk"
gcTime: 10 * 60_000, // 10 minutter i cache
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
refetchOnWindowFocus: true, // Refresh ved tab-focus
refetchOnReconnect: true, // Refresh efter netværkstab
},
},
});
Konklusion
TanStack Query er det rigtige valg til IoT-dashboards: Automatisk caching eliminerer redundante API-kald, polling giver live-opdateringer uden WebSocket-kompleksitet og optimistic updates giver hurtig UI-respons. Hierarkiske query-keys giver præcis cache-invalidering — kun det nødvendige refreshes.