From 253e2881b84d65b4675ef300c8513cb4602a8fe2 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 19:15:28 -0500 Subject: [PATCH] fix(lotw): stop /lotw page from refetching in an infinite loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /lotw page hammered /api/stations, /api/lotw/upload, and /api/lotw/download continuously because of a triple-cycle dependency tangle: 1. useEffect deps include `loading` and `loadData` 2. loadData calls setLoading(true) on entry, setLoading(false) on finish — flipping `loading` each call retriggers useEffect 3. loadData's useCallback deps include `selectedStation`, and loadData calls setSelectedStation(...) to pick a default — the state change reissues loadData's identity, which is also a dep of useEffect Each of #1 or #2/#3 alone is enough to loop. Combined, the page re-rendered constantly and hit three API endpoints on each pass. Fix: - loadData useCallback deps = [] (no longer a function of selectedStation) - setSelectedStation uses a functional updater so it can derive the default from the freshly-fetched stations without depending on selectedStation in deps - useEffect waits on UserContext's `loading` (not this component's), so the auth gate doesn't redirect during the initial /api/user check Net: page mounts → one fetch round → idle. Manual refresh button and post-action reloads still call loadData explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/lotw/page.tsx | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/app/lotw/page.tsx b/src/app/lotw/page.tsx index 9813145..585b908 100644 --- a/src/app/lotw/page.tsx +++ b/src/app/lotw/page.tsx @@ -67,55 +67,60 @@ export default function LotwPage() { const [certPassword, setCertPassword] = useState(''); const [showCertPassword, setShowCertPassword] = useState(false); - const { user } = useUser(); + const { user, loading: userLoading } = useUser(); const router = useRouter(); + // loadData is intentionally dep-free. setSelectedStation uses a functional + // updater so we don't need selectedStation in deps, and setLoading flips + // are internal — including them in useEffect's deps would create an + // infinite refetch loop (page used to hammer /api/stations + /api/lotw/*). const loadData = useCallback(async () => { try { setLoading(true); - - // Load stations + const stationsResponse = await fetch('/api/stations'); if (stationsResponse.ok) { const stationsData = await stationsResponse.json(); - setStations(stationsData.stations || []); - - // Set default station if none selected - if (!selectedStation && stationsData.stations?.length > 0) { - const defaultStation = stationsData.stations.find((s: Station) => s.is_default) || stationsData.stations[0]; - setSelectedStation(defaultStation.id.toString()); - } + const fetchedStations: Station[] = stationsData.stations || []; + setStations(fetchedStations); + + // Pick a default station only if one isn't already chosen. + setSelectedStation((prev) => { + if (prev) return prev; + const def = fetchedStations.find((s) => s.is_default) || fetchedStations[0]; + return def ? def.id.toString() : ''; + }); } - // Load upload logs const uploadResponse = await fetch('/api/lotw/upload'); if (uploadResponse.ok) { const uploadData = await uploadResponse.json(); setUploadLogs(uploadData.upload_logs || []); } - // Load download logs const downloadResponse = await fetch('/api/lotw/download'); if (downloadResponse.ok) { const downloadData = await downloadResponse.json(); setDownloadLogs(downloadData.download_logs || []); } - } catch (error) { console.error('Failed to load data:', error); setMessage({ type: 'error', text: 'Failed to load LoTW data' }); } finally { setLoading(false); } - }, [selectedStation]); + }, []); useEffect(() => { - if (!user && !loading) { + // Wait for the auth context to finish its initial /api/user check before + // deciding anything — user is null both pre-resolve and when unauthed. + if (userLoading) return; + if (!user) { router.push('/login'); return; } loadData(); - }, [user, router, loading, loadData]); + }, [user, userLoading, router, loadData]); const handleUpload = async () => { if (!selectedStation) {