M-Bus Gateway
← Tilbage til blog
· 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.

Se React Recharts guide eller portefølje-analyse suite.