Skip to content

Commit 60fa5c7

Browse files
patrickrbclaude
andauthored
fix(lotw): stop /lotw page from refetching in an infinite loop (#208)
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) <noreply@anthropic.com>
1 parent 08f96e8 commit 60fa5c7

1 file changed

Lines changed: 21 additions & 16 deletions

File tree

src/app/lotw/page.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,55 +67,60 @@ export default function LotwPage() {
6767
const [certPassword, setCertPassword] = useState('');
6868
const [showCertPassword, setShowCertPassword] = useState(false);
6969

70-
const { user } = useUser();
70+
const { user, loading: userLoading } = useUser();
7171
const router = useRouter();
7272

73+
// loadData is intentionally dep-free. setSelectedStation uses a functional
74+
// updater so we don't need selectedStation in deps, and setLoading flips
75+
// are internal — including them in useEffect's deps would create an
76+
// infinite refetch loop (page used to hammer /api/stations + /api/lotw/*).
7377
const loadData = useCallback(async () => {
7478
try {
7579
setLoading(true);
76-
77-
// Load stations
80+
7881
const stationsResponse = await fetch('/api/stations');
7982
if (stationsResponse.ok) {
8083
const stationsData = await stationsResponse.json();
81-
setStations(stationsData.stations || []);
82-
83-
// Set default station if none selected
84-
if (!selectedStation && stationsData.stations?.length > 0) {
85-
const defaultStation = stationsData.stations.find((s: Station) => s.is_default) || stationsData.stations[0];
86-
setSelectedStation(defaultStation.id.toString());
87-
}
84+
const fetchedStations: Station[] = stationsData.stations || [];
85+
setStations(fetchedStations);
86+
87+
// Pick a default station only if one isn't already chosen.
88+
setSelectedStation((prev) => {
89+
if (prev) return prev;
90+
const def = fetchedStations.find((s) => s.is_default) || fetchedStations[0];
91+
return def ? def.id.toString() : '';
92+
});
8893
}
8994

90-
// Load upload logs
9195
const uploadResponse = await fetch('/api/lotw/upload');
9296
if (uploadResponse.ok) {
9397
const uploadData = await uploadResponse.json();
9498
setUploadLogs(uploadData.upload_logs || []);
9599
}
96100

97-
// Load download logs
98101
const downloadResponse = await fetch('/api/lotw/download');
99102
if (downloadResponse.ok) {
100103
const downloadData = await downloadResponse.json();
101104
setDownloadLogs(downloadData.download_logs || []);
102105
}
103-
104106
} catch (error) {
105107
console.error('Failed to load data:', error);
106108
setMessage({ type: 'error', text: 'Failed to load LoTW data' });
107109
} finally {
108110
setLoading(false);
109111
}
110-
}, [selectedStation]);
112+
}, []);
111113

112114
useEffect(() => {
113-
if (!user && !loading) {
115+
// Wait for the auth context to finish its initial /api/user check before
116+
// deciding anything — user is null both pre-resolve and when unauthed.
117+
if (userLoading) return;
118+
if (!user) {
114119
router.push('/login');
115120
return;
116121
}
117122
loadData();
118-
}, [user, router, loading, loadData]);
123+
}, [user, userLoading, router, loadData]);
119124

120125
const handleUpload = async () => {
121126
if (!selectedStation) {

0 commit comments

Comments
 (0)