From dce1fbdb0956675d3bb072821cd532afe9cc0d6b Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Sat, 22 Aug 2026 13:16:29 +0800 Subject: [PATCH] fix: cancel in-flight DashboardPage lookups with AbortController - Abort the previous fetch when the user triggers a new lookup so stale responses can't overwrite newer results - Reuse the controller's signal for the escrow enrichment fetches - Guard state updates with signal.aborted (covers unmount + re-lookup races) - Wrap lookup in useCallback to enable stable memoization Fixes #203 --- frontend/src/pages/DashboardPage.tsx | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index b823ba2..464a8b1 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { ShieldCheck } from 'lucide-react'; import Spinner from '../components/Spinner'; import EmptyState from '../components/EmptyState'; @@ -25,21 +25,30 @@ export default function DashboardPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searched, setSearched] = useState(false); + const abortControllerRef = useRef(null); + + const lookup = useCallback(async () => { + const key = publicKey.trim(); + if (!key) return; + + // Cancel any in-flight lookup so stale responses can't overwrite newer results. + abortControllerRef.current?.abort(); + const controller = new AbortController(); + abortControllerRef.current = controller; + const { signal } = controller; - const lookup = async () => { - if (!publicKey.trim()) return; setLoading(true); setError(null); setSearched(true); try { - const res = await fetch(`${API_BASE}/enrollments/${encodeURIComponent(publicKey.trim())}`); + const res = await fetch(`${API_BASE}/enrollments/${encodeURIComponent(key)}`, { signal }); if (res.status === 404) { setRecords([]); return; } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json() as PositionRecord[]; if (Array.isArray(data)) { const enriched = await Promise.all(data.map(async (r) => { try { - const eRes = await fetch(`${API_BASE}/escrow/${encodeURIComponent(`${r.queueId}:${r.identity}`)}`); + const eRes = await fetch(`${API_BASE}/escrow/${encodeURIComponent(`${r.queueId}:${r.identity}`)}`, { signal }); if (eRes.ok) { const eData = await eRes.json(); r.escrowStatus = eData.status; @@ -52,11 +61,19 @@ export default function DashboardPage() { setRecords([]); } } catch (err: unknown) { + // A cancelled request is expected when the user triggers a new lookup + // or the component unmounts — don't surface it as an error. + if (signal.aborted) return; setError(err instanceof Error ? err.message : 'Network error'); } finally { - setLoading(false); + if (!signal.aborted) setLoading(false); } - }; + }, [publicKey]); + + // Abort any in-flight request when the component unmounts. + useEffect(() => { + return () => abortControllerRef.current?.abort(); + }, []); const active = records.filter((r) => !r.cancelled);