Custom Hooks
React hooks that bridge the API client, Zustand stores, and TanStack Query for the MetaForge Digital Twin Dashboard. Every data-fetching hook uses TanStack Query v5 for caching, deduplication, and background refetching. WebSocket events trigger targeted query invalidations so the UI stays in sync without polling.
src/hooks/use-websocket.ts
Lifecycle hook that opens (and tears down) the WebSocket connection from the websocket store.
import { useEffect } from "react";
import { useWebSocketStore } from "@/store/websocket-store";
const WS_URL = import.meta.env.VITE_WS_URL ?? `ws://${window.location.host}/ws`;
/**
* Initialise the WebSocket connection on mount and disconnect on unmount.
* Safe to call from multiple components — the store prevents duplicate
* connections.
*/
export function useWebSocket() {
const status = useWebSocketStore((s) => s.status);
const lastMessage = useWebSocketStore((s) => s.lastMessage);
const send = useWebSocketStore((s) => s.send);
const connect = useWebSocketStore((s) => s.connect);
const disconnect = useWebSocketStore((s) => s.disconnect);
useEffect(() => {
connect(WS_URL);
return () => disconnect();
}, [connect, disconnect]);
return { status, lastMessage, send } as const;
}
src/hooks/use-event-stream.ts
Subscribes to incoming WebSocket messages and maps each event type to the appropriate TanStack Query invalidation so cached data is refreshed automatically.
import { useEffect, useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useWebSocketStore, type WebSocketMessage } from "@/store/websocket-store";
// ---------------------------------------------------------------------------
// Event-to-query mapping
// ---------------------------------------------------------------------------
interface InvalidationRule {
/** Query keys to invalidate when this event arrives. */
keys: readonly (readonly string[])[];
}
const EVENT_MAP: Record<string, InvalidationRule> = {
"session.started": {
keys: [["sessions"], ["status"]],
},
"agent.progress": {
keys: [["agents"]],
// NOTE: session-specific invalidation is handled below because the
// session ID comes from the event payload.
},
"session.complete": {
keys: [["sessions"], ["pending"], ["status"]],
},
"bom.updated": {
keys: [["bom"], ["bom-risk"], ["supply-chain-risks"]],
},
"test.completed": {
keys: [["testing"], ["compliance"]],
},
"approval.requested": {
keys: [["approvals"], ["pending"], ["sessions"]],
},
"compliance.updated": {
keys: [["compliance"]],
},
};
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
/**
* Listens to the WebSocket message stream and keeps TanStack Query caches
* fresh by invalidating the right keys whenever a server event arrives.
*
* Returns the latest event so components can display toasts or banners.
*/
export function useEventStream() {
const queryClient = useQueryClient();
const lastMessage = useWebSocketStore((s) => s.lastMessage);
const [latestEvent, setLatestEvent] = useState<WebSocketMessage | null>(null);
const prevRef = useRef<WebSocketMessage | null>(null);
useEffect(() => {
// Only react to genuinely new messages.
if (!lastMessage || lastMessage === prevRef.current) return;
prevRef.current = lastMessage;
setLatestEvent(lastMessage);
const rule = EVENT_MAP[lastMessage.event];
if (!rule) return;
// Invalidate every listed query key.
for (const key of rule.keys) {
queryClient.invalidateQueries({ queryKey: [...key] });
}
// agent.progress carries a session ID — invalidate that specific session.
if (
lastMessage.event === "agent.progress" &&
typeof lastMessage.data === "object" &&
lastMessage.data !== null &&
"session_id" in lastMessage.data
) {
const sessionId = (lastMessage.data as { session_id: string }).session_id;
queryClient.invalidateQueries({ queryKey: ["session", sessionId] });
}
// session.complete also carries a session ID.
if (
lastMessage.event === "session.complete" &&
typeof lastMessage.data === "object" &&
lastMessage.data !== null &&
"session_id" in lastMessage.data
) {
const sessionId = (lastMessage.data as { session_id: string }).session_id;
queryClient.invalidateQueries({ queryKey: ["session", sessionId] });
}
}, [lastMessage, queryClient]);
return { latestEvent };
}
src/hooks/use-sessions.ts
TanStack Query hooks for session CRUD.
import {
useQuery,
useMutation,
useQueryClient,
type UseQueryOptions,
} from "@tanstack/react-query";
import {
getSessions,
getSession,
createSession,
deleteSession,
type GetSessionsParams,
type CreateSessionPayload,
} from "@/api/endpoints/sessions";
import type { PaginatedResponse, Session } from "@/types/session";
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
export function useSessions(
params?: GetSessionsParams,
options?: Partial<UseQueryOptions<PaginatedResponse<Session>>>,
) {
return useQuery<PaginatedResponse<Session>>({
queryKey: ["sessions", params],
queryFn: () => getSessions(params),
staleTime: 30_000,
...options,
});
}
export function useSession(
id: string | undefined,
options?: Partial<UseQueryOptions<Session>>,
) {
return useQuery<Session>({
queryKey: ["session", id],
queryFn: () => getSession(id!),
enabled: !!id,
staleTime: 10_000,
...options,
});
}
// ---------------------------------------------------------------------------
// Mutations
// ---------------------------------------------------------------------------
export function useCreateSession() {
const queryClient = useQueryClient();
return useMutation<Session, Error, CreateSessionPayload>({
mutationFn: createSession,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["status"] });
},
});
}
export function useDeleteSession() {
const queryClient = useQueryClient();
return useMutation<void, Error, string>({
mutationFn: deleteSession,
onSuccess: (_data, sessionId) => {
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.removeQueries({ queryKey: ["session", sessionId] });
queryClient.invalidateQueries({ queryKey: ["status"] });
},
});
}
src/hooks/use-agents.ts
Agent listing and per-agent status polling.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getAgents, getAgentStatus } from "@/api/endpoints/agents";
import type { Agent } from "@/types/agent";
/**
* Fetch all registered agents.
*/
export function useAgents(options?: Partial<UseQueryOptions<Agent[]>>) {
return useQuery<Agent[]>({
queryKey: ["agents"],
queryFn: getAgents,
staleTime: 60_000,
...options,
});
}
/**
* Poll a single agent's execution status.
*
* @param code - Agent identifier (e.g. "REQ", "EE", "FW").
*/
export function useAgentStatus(
code: string | undefined,
options?: Partial<UseQueryOptions<Agent>>,
) {
return useQuery<Agent>({
queryKey: ["agent", code],
queryFn: () => getAgentStatus(code!),
enabled: !!code,
refetchInterval: 5_000,
staleTime: 3_000,
...options,
});
}
src/hooks/use-health.ts
System health and status with automatic background polling.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getHealth, getStatus } from "@/api/endpoints/health";
import type { HealthResponse, StatusResponse } from "@/types/system";
/**
* Lightweight liveness probe — polled every 30 seconds.
*/
export function useHealth(options?: Partial<UseQueryOptions<HealthResponse>>) {
return useQuery<HealthResponse>({
queryKey: ["health"],
queryFn: getHealth,
refetchInterval: 30_000,
staleTime: 25_000,
...options,
});
}
/**
* Detailed system status — polled every 10 seconds.
*/
export function useStatus(options?: Partial<UseQueryOptions<StatusResponse>>) {
return useQuery<StatusResponse>({
queryKey: ["status"],
queryFn: getStatus,
refetchInterval: 10_000,
staleTime: 8_000,
...options,
});
}
src/hooks/use-bom.ts
Bill of Materials data and risk analysis.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getBOM, getBOMRisk } from "@/api/endpoints/bom";
import type { BOMEntry, BOMRiskSummary } from "@/types/bom";
/**
* Fetch the full BOM table.
*/
export function useBOM(options?: Partial<UseQueryOptions<BOMEntry[]>>) {
return useQuery<BOMEntry[]>({
queryKey: ["bom"],
queryFn: getBOM,
staleTime: 60_000,
...options,
});
}
/**
* Fetch BOM risk analysis (single-source, EOL, lead-time flags).
*/
export function useBOMRisk(
options?: Partial<UseQueryOptions<BOMRiskSummary>>,
) {
return useQuery<BOMRiskSummary>({
queryKey: ["bom-risk"],
queryFn: getBOMRisk,
staleTime: 60_000,
...options,
});
}
src/hooks/use-compliance.ts
Per-market regulatory compliance data.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getCompliance } from "@/api/endpoints/compliance";
import type { ComplianceMarket, ComplianceSummary } from "@/types/compliance";
/**
* Fetch compliance status for a specific target market.
*
* @param market - One of "UKCA", "CE", or "FCC".
*/
export function useCompliance(
market: ComplianceMarket | undefined,
options?: Partial<UseQueryOptions<ComplianceSummary>>,
) {
return useQuery<ComplianceSummary>({
queryKey: ["compliance", market],
queryFn: () => getCompliance(market!),
enabled: !!market,
staleTime: 120_000,
...options,
});
}
src/hooks/use-digital-thread.ts
Digital-thread graph and single-requirement traceability.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import {
getDigitalThread,
getTraceability,
} from "@/api/endpoints/digital-thread";
import type { DigitalThreadGraph } from "@/types/digital-thread";
/**
* Fetch the full digital-thread graph.
*/
export function useDigitalThread(
options?: Partial<UseQueryOptions<DigitalThreadGraph>>,
) {
return useQuery<DigitalThreadGraph>({
queryKey: ["digital-thread"],
queryFn: getDigitalThread,
staleTime: 60_000,
...options,
});
}
/**
* Trace a single requirement through the digital thread.
*/
export function useTraceability(
reqId: string | undefined,
options?: Partial<UseQueryOptions<DigitalThreadGraph>>,
) {
return useQuery<DigitalThreadGraph>({
queryKey: ["traceability", reqId],
queryFn: () => getTraceability(reqId!),
enabled: !!reqId,
staleTime: 30_000,
...options,
});
}
src/hooks/use-supply-chain.ts
Supply-chain risk data.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getSupplyChainRisks } from "@/api/endpoints/supply-chain";
import type { SupplyChainRisk } from "@/types/supply-chain";
/**
* Fetch all current supply-chain risk entries.
*/
export function useSupplyChainRisks(
options?: Partial<UseQueryOptions<SupplyChainRisk[]>>,
) {
return useQuery<SupplyChainRisk[]>({
queryKey: ["supply-chain-risks"],
queryFn: getSupplyChainRisks,
staleTime: 60_000,
...options,
});
}
src/hooks/use-approvals.ts
Approval queue with approve/reject mutations.
import {
useQuery,
useMutation,
useQueryClient,
type UseQueryOptions,
} from "@tanstack/react-query";
import {
getPendingApprovals,
approveSession,
rejectSession,
} from "@/api/endpoints/approvals";
import type { Approval } from "@/types/approval";
/**
* Fetch all pending approvals. Refetches every 15 seconds.
*/
export function useApprovals(
options?: Partial<UseQueryOptions<Approval[]>>,
) {
return useQuery<Approval[]>({
queryKey: ["approvals"],
queryFn: getPendingApprovals,
refetchInterval: 15_000,
staleTime: 10_000,
...options,
});
}
/**
* Approve a pending session.
*/
export function useApproveSession() {
const queryClient = useQueryClient();
return useMutation<void, Error, { sessionId: string; comment?: string }>({
mutationFn: ({ sessionId, comment }) =>
approveSession(sessionId, comment),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["approvals"] });
queryClient.invalidateQueries({ queryKey: ["pending"] });
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["status"] });
},
});
}
/**
* Reject a pending session.
*/
export function useRejectSession() {
const queryClient = useQueryClient();
return useMutation<void, Error, { sessionId: string; reason: string }>({
mutationFn: ({ sessionId, reason }) =>
rejectSession(sessionId, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["approvals"] });
queryClient.invalidateQueries({ queryKey: ["pending"] });
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["status"] });
},
});
}
src/hooks/use-testing.ts
Test-coverage metrics.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getTestCoverage } from "@/api/endpoints/testing";
import type { TestCoverage } from "@/types/testing";
/**
* Fetch aggregate test-coverage data.
*/
export function useTestCoverage(
options?: Partial<UseQueryOptions<TestCoverage>>,
) {
return useQuery<TestCoverage>({
queryKey: ["testing"],
queryFn: getTestCoverage,
staleTime: 60_000,
...options,
});
}
src/hooks/use-gates.ts
Gate-readiness checks for hardware milestones.
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { getGateReadiness } from "@/api/endpoints/gates";
import type { GateReadiness, GateName } from "@/types/gate";
/**
* Fetch readiness status for a hardware milestone gate.
*
* @param gate - One of "EVT", "DVT", or "PVT".
*/
export function useGateReadiness(
gate: GateName | undefined,
options?: Partial<UseQueryOptions<GateReadiness>>,
) {
return useQuery<GateReadiness>({
queryKey: ["gate-readiness", gate],
queryFn: () => getGateReadiness(gate!),
enabled: !!gate,
staleTime: 60_000,
...options,
});
}
The following chat hooks are specified in Agent Chat Channel:
useChatThreads(),useChatThread(),useChatChannels(),useCreateChatThread(),useSendChatMessage().