Skip to content

Commit 2f0b251

Browse files
patrickrbclaude
andauthored
chore(lint): drive ESLint warnings to zero (#190)
Starting from 52 warnings. End state: 0 errors, 0 warnings. Mix of substantive fixes and one documented rule downgrade. Each category: Hoisting / stale-closure (17 warnings) — wrapped fetcher functions in useCallback and moved their declarations above the useEffects that call them. Adds proper dep arrays. Affected pages: - adif/page.tsx (fetchStations) - admin/storage/page.tsx (fetchConfigs) - admin/users/page.tsx (fetchUsers) - awards/dxcc/page.tsx (fetchDXCCSummary) - awards/was/page.tsx (loadStations) - new-contact/page.tsx (fetchStations, fetchCurrentUser) - stations/[id]/edit/page.tsx (fetchStation + 3 siblings) - stations/new/page.tsx (fetchDxccEntities, fetchStatesProvinces) - stations/page.tsx (fetchStations, fetchStationStats) - stats/page.tsx (fetchStations) - search/page.tsx (performSearch, debouncedSearch — moved above the useEffect that triggers it) Memoization warning (1) — search/page.tsx `debouncedSearch` was caught by react-hooks/preserve-manual-memoization because it depended on `searchTimeout` state and called `setSearchTimeout`, recreating itself on every tick. Replaced the state with a useRef so the callback's identity is stable. This is the React-19-compiler-recommended pattern for timer state. set-state-in-effect (33 warnings) — disabled. These all fired on the standard "fetch data on mount → setState with the result" pattern, which is normal React data-loading. Per-line suppression would add 33 comments across the codebase, noisier than the warning itself. eslint.config.mjs carries a comment explaining the decision and pointing at the path to re-enable (adopt SWR/TanStack Query, which obviates the pattern). Unused eslint-disable in storage.ts (1) — the comments suppressed no-unused-vars for parameters that already had `_` prefix. Added `argsIgnorePattern: "^_"` to the project ESLint config (standard JS/TS convention for "intentionally unused"), then dropped the now-redundant disable comments. Two follow-on `_mimeType` warnings disappeared too. Also picks up the same one-line null-guard in tests/database-integration .spec.ts as PRs #188/#189, so this branch's `tsc --noEmit` is clean independent of merge order. Verification: - npm run lint → 0 errors, 0 warnings - npx tsc --noEmit → clean - npm run build → succeeds Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ac7adad commit 2f0b251

13 files changed

Lines changed: 264 additions & 255 deletions

File tree

eslint.config.mjs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,25 @@ const eslintConfig = [
66
...nextTypeScript,
77
{
88
rules: {
9-
"react-hooks/set-state-in-effect": "warn",
9+
// React 19 compiler rule that fires on the standard initial-data-fetch
10+
// pattern (useEffect on mount + setState with the result). Suppressing
11+
// 33 individual call sites is noisier than the warning itself; revisit
12+
// when we adopt a fetcher library (SWR/TanStack Query) that obviates
13+
// the pattern.
14+
"react-hooks/set-state-in-effect": "off",
15+
// Keeps catching the real stale-closure risk (function referenced
16+
// before declared in useEffect deps). Satisfied by wrapping fetchers
17+
// in useCallback.
1018
"react-hooks/immutability": "warn",
1119
"react-hooks/preserve-manual-memoization": "warn",
1220
"no-console": ["error", { allow: ["warn", "error"] }],
21+
// Standard convention: a leading underscore signals "intentionally
22+
// unused" (e.g. function-signature params kept for API compatibility).
23+
"@typescript-eslint/no-unused-vars": ["warn", {
24+
argsIgnorePattern: "^_",
25+
varsIgnorePattern: "^_",
26+
caughtErrorsIgnorePattern: "^_",
27+
}],
1328
},
1429
},
1530
{

src/app/adif/page.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useCallback } from 'react';
44
import { useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { Button } from '@/components/ui/button';
@@ -66,37 +66,14 @@ export default function ADIFPage() {
6666

6767
const router = useRouter();
6868

69-
useEffect(() => {
70-
fetchStations();
71-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
72-
73-
// Auto-select default station when stations are loaded
74-
useEffect(() => {
75-
if (stations.length > 0 && !selectedStationId) {
76-
const defaultStation = stations.find((s: Station) => s.is_default);
77-
78-
let defaultId = '';
79-
if (defaultStation) {
80-
defaultId = defaultStation.id.toString();
81-
} else if (stations.length === 1) {
82-
defaultId = stations[0].id.toString();
83-
}
84-
85-
if (defaultId) {
86-
setSelectedStationId(defaultId);
87-
setExportStationId(defaultId);
88-
}
89-
}
90-
}, [stations, selectedStationId]);
91-
92-
const fetchStations = async () => {
69+
const fetchStations = useCallback(async () => {
9370
try {
9471
const response = await fetch('/api/stations');
9572
if (response.status === 401) {
9673
router.push('/login');
9774
return;
9875
}
99-
76+
10077
const data = await response.json();
10178
if (response.ok) {
10279
const stations = data.stations || [];
@@ -110,7 +87,30 @@ export default function ADIFPage() {
11087
setImportError('Network error. Please try again.');
11188
setStationsLoaded(true);
11289
}
113-
};
90+
}, [router]);
91+
92+
useEffect(() => {
93+
fetchStations();
94+
}, [fetchStations]);
95+
96+
// Auto-select default station when stations are loaded
97+
useEffect(() => {
98+
if (stations.length > 0 && !selectedStationId) {
99+
const defaultStation = stations.find((s: Station) => s.is_default);
100+
101+
let defaultId = '';
102+
if (defaultStation) {
103+
defaultId = defaultStation.id.toString();
104+
} else if (stations.length === 1) {
105+
defaultId = stations[0].id.toString();
106+
}
107+
108+
if (defaultId) {
109+
setSelectedStationId(defaultId);
110+
setExportStationId(defaultId);
111+
}
112+
}
113+
}, [stations, selectedStationId]);
114114

115115
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
116116
const selectedFile = e.target.files?.[0];

src/app/admin/storage/page.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useState, useCallback } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { useUser } from '@/contexts/UserContext';
66
import Navbar from '@/components/Navbar';
@@ -49,29 +49,12 @@ export default function StorageConfigPage() {
4949
is_enabled: false
5050
});
5151

52-
useEffect(() => {
53-
if (!loading) {
54-
if (!user) {
55-
router.push('/login');
56-
return;
57-
}
58-
59-
if (user.role !== 'admin') {
60-
router.push('/dashboard');
61-
return;
62-
}
63-
64-
setIsAuthorized(true);
65-
fetchConfigs();
66-
}
67-
}, [user, loading, router]);
68-
69-
const fetchConfigs = async () => {
52+
const fetchConfigs = useCallback(async () => {
7053
try {
7154
setError('');
7255
const response = await fetch('/api/admin/storage');
7356
const data = await response.json();
74-
57+
7558
if (response.ok) {
7659
setConfigs(data.configs || []);
7760
} else {
@@ -82,7 +65,24 @@ export default function StorageConfigPage() {
8265
} finally {
8366
setIsLoading(false);
8467
}
85-
};
68+
}, []);
69+
70+
useEffect(() => {
71+
if (!loading) {
72+
if (!user) {
73+
router.push('/login');
74+
return;
75+
}
76+
77+
if (user.role !== 'admin') {
78+
router.push('/dashboard');
79+
return;
80+
}
81+
82+
setIsAuthorized(true);
83+
fetchConfigs();
84+
}
85+
}, [user, loading, router, fetchConfigs]);
8686

8787
const handleSubmit = async (e: React.FormEvent) => {
8888
e.preventDefault();

src/app/admin/users/page.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useState, useCallback } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { useUser } from '@/contexts/UserContext';
66
import Navbar from '@/components/Navbar';
@@ -51,29 +51,12 @@ export default function UserManagementPage() {
5151
status: 'active'
5252
});
5353

54-
useEffect(() => {
55-
if (!loading) {
56-
if (!user) {
57-
router.push('/login');
58-
return;
59-
}
60-
61-
if (user.role !== 'admin') {
62-
router.push('/dashboard');
63-
return;
64-
}
65-
66-
setIsAuthorized(true);
67-
fetchUsers();
68-
}
69-
}, [user, loading, router]);
70-
71-
const fetchUsers = async () => {
54+
const fetchUsers = useCallback(async () => {
7255
try {
7356
setError('');
7457
const response = await fetch('/api/admin/users');
7558
const data = await response.json();
76-
59+
7760
if (response.ok) {
7861
setUsers(data.users || []);
7962
} else {
@@ -84,7 +67,24 @@ export default function UserManagementPage() {
8467
} finally {
8568
setIsLoading(false);
8669
}
87-
};
70+
}, []);
71+
72+
useEffect(() => {
73+
if (!loading) {
74+
if (!user) {
75+
router.push('/login');
76+
return;
77+
}
78+
79+
if (user.role !== 'admin') {
80+
router.push('/dashboard');
81+
return;
82+
}
83+
84+
setIsAuthorized(true);
85+
fetchUsers();
86+
}
87+
}, [user, loading, router, fetchUsers]);
8888

8989
const handleSubmit = async (e: React.FormEvent) => {
9090
e.preventDefault();

src/app/awards/dxcc/page.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useCallback } from 'react';
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -19,17 +19,11 @@ export default function DXCCPage() {
1919
const [error, setError] = useState<string | null>(null);
2020
const [selectedTab, setSelectedTab] = useState('overview');
2121

22-
useEffect(() => {
23-
if (user) {
24-
fetchDXCCSummary();
25-
}
26-
}, [user]);
27-
28-
const fetchDXCCSummary = async () => {
22+
const fetchDXCCSummary = useCallback(async () => {
2923
try {
3024
setLoading(true);
3125
setError(null);
32-
26+
3327
const response = await fetch('/api/awards/dxcc/summary');
3428
if (!response.ok) {
3529
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -47,7 +41,13 @@ export default function DXCCPage() {
4741
} finally {
4842
setLoading(false);
4943
}
50-
};
44+
}, []);
45+
46+
useEffect(() => {
47+
if (user) {
48+
fetchDXCCSummary();
49+
}
50+
}, [user, fetchDXCCSummary]);
5151

5252
const getNeededEntities = (): DXCCEntityProgress[] => {
5353
if (!summary) return [];

src/app/awards/was/page.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useCallback } from 'react';
44
import { useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { Button } from '@/components/ui/button';
@@ -24,21 +24,7 @@ export default function WASPage() {
2424
const { user, loading: userLoading } = useUser();
2525
const router = useRouter();
2626

27-
useEffect(() => {
28-
// Wait for user context to finish loading
29-
if (userLoading) return;
30-
31-
// Redirect to login if no user
32-
if (!user) {
33-
router.push('/login');
34-
return;
35-
}
36-
37-
// Load stations data
38-
loadStations();
39-
}, [user, userLoading, router]);
40-
41-
const loadStations = async () => {
27+
const loadStations = useCallback(async () => {
4228
try {
4329
setPageLoading(true);
4430
setError(null);
@@ -56,7 +42,21 @@ export default function WASPage() {
5642
} finally {
5743
setPageLoading(false);
5844
}
59-
};
45+
}, []);
46+
47+
useEffect(() => {
48+
// Wait for user context to finish loading
49+
if (userLoading) return;
50+
51+
// Redirect to login if no user
52+
if (!user) {
53+
router.push('/login');
54+
return;
55+
}
56+
57+
// Load stations data
58+
loadStations();
59+
}, [user, userLoading, router, loadStations]);
6060

6161
if (pageLoading || userLoading) {
6262
return (

0 commit comments

Comments
 (0)