Skip to content

Commit e920bc1

Browse files
committed
fix(dashboard): migrate to react-query for dedupe and cancellation
1 parent 1bb26fc commit e920bc1

2 files changed

Lines changed: 40 additions & 54 deletions

File tree

frontend/src/components/dashboard/dashboard-view.tsx

Lines changed: 13 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import toast from "react-hot-toast";
1616
*/
1717

1818
import { Skeleton } from "@/components/ui/Skeleton";
19+
import { useQueryClient } from "@tanstack/react-query";
1920
import {
2021
getDashboardAnalytics,
2122
fetchDashboardData,
23+
useDashboard,
24+
dashboardQueryKey,
2225
type DashboardSnapshot,
2326
type Stream,
2427
} from "@/lib/dashboard";
@@ -440,13 +443,13 @@ function renderRecentActivity(snapshot: DashboardSnapshot | null, onCreateStream
440443
// ─── Main Component ───────────────────────────────────────────────────────────
441444

442445
export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
446+
const queryClient = useQueryClient();
443447
const [activeTab, setActiveTab] = React.useState("overview");
444448
const [showWizard, setShowWizard] = React.useState(false);
445449
const [modal, setModal] = React.useState<ModalState>(null);
446450

447-
const [snapshot, setSnapshot] = React.useState<DashboardSnapshot | null>(null);
448-
const [isSnapshotLoading, setIsSnapshotLoading] = React.useState(true);
449-
const [snapshotError, setSnapshotError] = React.useState<string | null>(null);
451+
const { data: snapshot = null, isLoading: isSnapshotLoading, error: queryError, refetch } = useDashboard(session.publicKey);
452+
const snapshotError = queryError ? (queryError as Error).message : null;
450453

451454
const { events: streamEvents, connected, reconnecting, error } = useStreamEvents({
452455
userPublicKeys: [session.publicKey],
@@ -459,15 +462,11 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
459462
if (latestEvent) {
460463
const relevantTypes = ["created", "topped_up", "withdrawn", "cancelled", "completed", "paused", "resumed"];
461464
if (relevantTypes.includes(latestEvent.type)) {
462-
fetchDashboardData(session.publicKey)
463-
.then(setSnapshot)
464-
.catch((err) => {
465-
setSnapshotError(err instanceof Error ? err.message : "Failed to refresh dashboard");
466-
});
465+
queryClient.invalidateQueries({ queryKey: dashboardQueryKey(session.publicKey) });
467466
}
468467
}
469468
}
470-
}, [streamEvents, session.publicKey]);
469+
}, [streamEvents, session.publicKey, queryClient]);
471470

472471
const [streamForm, setStreamForm] = React.useState<StreamFormValues>(EMPTY_STREAM_FORM);
473472
const [templates, setTemplates] = React.useState<StreamTemplate[]>([]);
@@ -524,42 +523,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
524523
persistTemplates(templates);
525524
}, [templates, templatesHydrated]);
526525

527-
// ── Load dashboard snapshot ───────────────────────────────────────────────
528-
529-
const loadSnapshot = React.useCallback(async () => {
530-
setIsSnapshotLoading(true);
531-
setSnapshotError(null);
532-
try {
533-
const next = await fetchDashboardData(session.publicKey);
534-
setSnapshot(next);
535-
} catch (err) {
536-
setSnapshot(null);
537-
setSnapshotError(err instanceof Error ? err.message : "Failed to fetch dashboard data.");
538-
} finally {
539-
setIsSnapshotLoading(false);
540-
}
541-
}, [session.publicKey, setIsSnapshotLoading, setSnapshotError, setSnapshot]);
542-
543-
React.useEffect(() => {
544-
let cancelled = false;
545-
const run = async () => {
546-
setIsSnapshotLoading(true);
547-
setSnapshotError(null);
548-
try {
549-
const next = await fetchDashboardData(session.publicKey);
550-
if (!cancelled) setSnapshot(next);
551-
} catch (err) {
552-
if (!cancelled) {
553-
setSnapshot(null);
554-
setSnapshotError(err instanceof Error ? err.message : "Failed to fetch dashboard data.");
555-
}
556-
} finally {
557-
if (!cancelled) setIsSnapshotLoading(false);
558-
}
559-
};
560-
void run();
561-
return () => { cancelled = true; };
562-
}, [session.publicKey]);
526+
const loadSnapshot = () => refetch();
563527

564528
// ── Template handlers ─────────────────────────────────────────────────────
565529

@@ -619,19 +583,19 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
619583
// ── Optimistic helpers ────────────────────────────────────────────────────
620584

621585
const removeStreamLocally = (streamId: string) => {
622-
setSnapshot((prev) => {
586+
queryClient.setQueryData<DashboardSnapshot | undefined>(dashboardQueryKey(session.publicKey), (prev) => {
623587
if (!prev) return prev;
624588
return { ...prev, outgoingStreams: prev.outgoingStreams.map((s) => s.id === streamId ? { ...s, status: "Cancelled", isActive: false } : s), activeStreamsCount: Math.max(0, prev.activeStreamsCount - 1) };
625589
});
626590
};
627591

628592
const topUpStreamLocally = (streamId: string, amount: number) => {
629-
setSnapshot((prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: prev.outgoingStreams.map((s) => s.id === streamId ? { ...s, deposited: s.deposited + amount } : s) }; });
593+
queryClient.setQueryData<DashboardSnapshot | undefined>(dashboardQueryKey(session.publicKey), (prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: prev.outgoingStreams.map((s) => s.id === streamId ? { ...s, deposited: s.deposited + amount } : s) }; });
630594
};
631595

632596
const addStreamLocally = (data: StreamFormData) => {
633597
const newStream: Stream = { id: `stream-${Date.now()}`, date: new Date().toISOString().split("T")[0] ?? "", recipient: shortenPublicKey(data.recipient), amount: parseFloat(data.amount), token: data.token, status: "Active", deposited: parseFloat(data.amount), withdrawn: 0, ratePerSecond: 0, lastUpdateTime: Math.floor(Date.now() / 1000), isActive: true };
634-
setSnapshot((prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: [newStream, ...prev.outgoingStreams], activeStreamsCount: prev.activeStreamsCount + 1 }; });
598+
queryClient.setQueryData<DashboardSnapshot | undefined>(dashboardQueryKey(session.publicKey), (prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: [newStream, ...prev.outgoingStreams], activeStreamsCount: prev.activeStreamsCount + 1 }; });
635599
};
636600

637601
// ── Contract handlers ─────────────────────────────────────────────────────
@@ -685,8 +649,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
685649
setWithdrawingIncomingStreamId(stream.id);
686650
try {
687651
await sorobanWithdraw(session, { streamId: BigInt(stream.id.replace(/\D/g, "") || "0") });
688-
const refreshed = await fetchDashboardData(session.publicKey);
689-
setSnapshot(refreshed);
652+
await queryClient.invalidateQueries({ queryKey: dashboardQueryKey(session.publicKey) });
690653
toast.success("Withdrawal successful!", { id: toastId });
691654
} catch (err) {
692655
toast.error(toSorobanErrorMessage(err), { id: toastId });

frontend/src/lib/dashboard.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useQuery } from "@tanstack/react-query";
12
import type { BackendStream } from "./api-types";
23
import { getStreamsEndpointCandidates, toTokenAmount } from "./api/_shared";
34
import { TOKEN_ADDRESSES } from "./soroban";
@@ -78,13 +79,15 @@ function mapStreamStatus(s: BackendStream): Stream["status"] {
7879
async function fetchStreams(
7980
publicKey: string,
8081
role: "sender" | "recipient",
82+
signal?: AbortSignal
8183
): Promise<BackendStream[]> {
8284
const endpoints = getStreamsEndpointCandidates();
8385
const params = new URLSearchParams({ [role]: publicKey });
8486
let lastError: Error | null = null;
8587

8688
for (const endpoint of endpoints) {
87-
const response = await fetch(`${endpoint}?${params.toString()}`);
89+
try {
90+
const response = await fetch(`${endpoint}?${params.toString()}`, { signal });
8891
if (response.ok) {
8992
const payload = (await response.json()) as
9093
| BackendStream[]
@@ -100,6 +103,14 @@ async function fetchStreams(
100103
lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`);
101104
}
102105

106+
} catch (err) {
107+
if (err instanceof Error && err.name === "AbortError") {
108+
throw err;
109+
}
110+
lastError = err instanceof Error ? err : new Error(String(err));
111+
}
112+
}
113+
103114
throw lastError ?? new Error("Failed to fetch streams from backend.");
104115
}
105116

@@ -129,11 +140,11 @@ export function mapBackendStreamToFrontend(s: BackendStream, counterparty: strin
129140
/**
130141
* Fetches dashboard data for a given public key by querying both outgoing and incoming streams.
131142
*/
132-
export async function fetchDashboardData(publicKey: string): Promise<DashboardSnapshot> {
143+
export async function fetchDashboardData(publicKey: string, signal?: AbortSignal): Promise<DashboardSnapshot> {
133144
try {
134145
const [outgoing, incoming] = await Promise.all([
135-
fetchStreams(publicKey, "sender"),
136-
fetchStreams(publicKey, "recipient"),
146+
fetchStreams(publicKey, "sender", signal),
147+
fetchStreams(publicKey, "recipient", signal),
137148
]);
138149

139150
const outgoingStreams = outgoing.map((stream) =>
@@ -309,3 +320,15 @@ export function getDashboardAnalytics(
309320
},
310321
];
311322
}
323+
324+
export function dashboardQueryKey(publicKey: string) {
325+
return ["dashboard", publicKey] as const;
326+
}
327+
328+
export function useDashboard(publicKey: string) {
329+
return useQuery({
330+
queryKey: dashboardQueryKey(publicKey),
331+
queryFn: ({ signal }) => fetchDashboardData(publicKey, signal),
332+
enabled: Boolean(publicKey),
333+
});
334+
}

0 commit comments

Comments
 (0)