Skip to content

Commit 6fb5fcb

Browse files
Sbussisoclaude
andcommitted
chore(eslint): wire up flat config + fix one real ref-in-render bug
Pre-this-commit, ``npm run lint`` had been failing silently with "ESLint couldn't find an eslint.config.(js|mjs|cjs) file" — the project shipped eslint v9 + eslint-plugin-react-hooks + eslint-plugin-react-refresh as devDependencies but no config file, so lint had been a no-op for an unknown amount of time. frontend/eslint.config.js (new) ------------------------------- Flat-config wiring for ESLint 9. Pulls in: - js.configs.recommended - eslint-plugin-react-hooks v7 recommended ruleset (which now includes exhaustive-deps + the new React-Compiler advisory rules) - eslint-plugin-react-refresh's only-export-components rule - vitest globals scoped to ``tests/**`` Tunings applied (all explained in inline comments): - ``no-unused-vars`` ignores PascalCase identifiers because the bare rule doesn't track JSX usage (`<Foo />`) and would false-positive every component import. The dedicated ``react/jsx-uses-vars`` rule would do this properly but pulling in eslint-plugin-react for one rule is overkill; the regex covers our patterns. - ``no-empty`` allows empty catch blocks — we have a few that intentionally swallow malformed-localStorage parses. - The four new react-hooks v7 React-Compiler advisory rules (set-state-in-effect, purity, refs, preserve-manual-memoization) demoted from error to warn. They flag patterns the compiler can't optimize, not patterns that are currently buggy. Fix when touching the surrounding code, not as a sweep. Real bug fixed -------------- hooks/useMotionAlerts.jsx — was assigning ``camerasRef.current = cameras`` *during render*, which violates React's purity rules and breaks under React Compiler / Strict Mode double-renders. Moved the assignment into ``useEffect(() => { camerasRef.current = cameras }, [cameras])`` so it runs after commit, which is what the SSE callback consumers expected in the first place. No observable behaviour change for users (the callback still always sees the post-commit cameras map). Verification ------------ npm run lint — 0 errors, 26 warnings (was 163 problems / unable to run) npm test — 37/37 passing npm run build — clean (436ms) The 26 remaining warnings are all advisory (exhaustive-deps where adding the missing dep would cause unnecessary re-renders, the four React-Compiler rules listed above, and react-refresh/only-export-components on the four hook+context files where the standard fix is a structural file split). Each is documented in the lint output with a clear path to resolution; we'll burn them down opportunistically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent eefbddf commit 6fb5fcb

2 files changed

Lines changed: 124 additions & 2 deletions

File tree

frontend/eslint.config.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// ESLint flat config (eslint v9+).
2+
//
3+
// Pre-this-file the project shipped eslint + eslint-plugin-react-hooks +
4+
// eslint-plugin-react-refresh as devDependencies and a `npm run lint`
5+
// script, but no actual config file — meaning `eslint .` silently errored
6+
// with "couldn't find a config" and the lint command had been a no-op for
7+
// however long. This file fixes that, with a deliberate focus on rules
8+
// that catch real bugs (react-hooks/exhaustive-deps) over stylistic ones.
9+
//
10+
// What we enable
11+
// ---------------
12+
// - `js.configs.recommended` — base set: no-undef, no-redeclare, etc.
13+
// - `react-hooks/recommended` (v7+ flat) — rules-of-hooks + exhaustive-deps
14+
// - `react-refresh/only-export-components`— Vite HMR safety
15+
//
16+
// What we tone down
17+
// -----------------
18+
// - `no-unused-vars` — warn (not error) and ignore args starting with `_`
19+
// so tests / event handlers can take unused params without ceremony
20+
// - `no-empty` — error, but allow empty catch blocks that explicitly
21+
// intend to swallow (we have a few in localStorage parsing where the
22+
// message is "we got malformed JSON; ignore and move on")
23+
//
24+
// Test files get vitest globals (describe/it/expect/vi/...) so a future
25+
// test author doesn't have to import them or chase a no-undef warning.
26+
27+
import js from "@eslint/js"
28+
import reactHooks from "eslint-plugin-react-hooks"
29+
import reactRefresh from "eslint-plugin-react-refresh"
30+
import globals from "globals"
31+
32+
export default [
33+
// Always ignore build outputs and vendored code.
34+
{
35+
ignores: ["dist/**", "node_modules/**", "public/**"],
36+
},
37+
38+
// Base rules + browser env for all source / config / test JS+JSX.
39+
{
40+
files: ["**/*.{js,jsx}"],
41+
languageOptions: {
42+
ecmaVersion: "latest",
43+
sourceType: "module",
44+
globals: {
45+
...globals.browser,
46+
...globals.node, // for vite.config.js
47+
},
48+
parserOptions: {
49+
ecmaFeatures: { jsx: true },
50+
},
51+
},
52+
plugins: {
53+
"react-hooks": reactHooks,
54+
"react-refresh": reactRefresh,
55+
},
56+
rules: {
57+
...js.configs.recommended.rules,
58+
...reactHooks.configs.recommended.rules,
59+
"react-refresh/only-export-components": [
60+
"warn",
61+
{ allowConstantExport: true },
62+
],
63+
// Quality-of-life — these match how the codebase already writes JS.
64+
//
65+
// ``varsIgnorePattern`` skips PascalCase identifiers because React
66+
// components used only in JSX (`<Foo />`) aren't treated as
67+
// references by the bare ``no-unused-vars`` rule — that's what
68+
// ``eslint-plugin-react``'s ``jsx-uses-vars`` would normally fix,
69+
// but pulling in another plugin for one rule is overkill. The
70+
// PascalCase regex covers ~all our import-of-component patterns;
71+
// the camelCase + lowercase ones still get flagged so we still
72+
// catch genuinely-unused locals.
73+
"no-unused-vars": [
74+
"warn",
75+
{
76+
argsIgnorePattern: "^_",
77+
varsIgnorePattern: "^[A-Z_]",
78+
caughtErrorsIgnorePattern: "^_",
79+
},
80+
],
81+
"no-empty": ["error", { allowEmptyCatch: true }],
82+
// react-hooks v7 ships a set of new rules that are (mostly)
83+
// forward-looking advisories from the React Compiler — they catch
84+
// patterns the compiler can't optimize, not patterns that are
85+
// currently buggy. Demote them to warn so CI passes while leaving
86+
// the diagnostic information visible. Each one has a textbook fix
87+
// (lazy useState, useMemo with no deps, etc.) that we should apply
88+
// when we touch the surrounding code, not as a single sweep.
89+
"react-hooks/set-state-in-effect": "warn",
90+
"react-hooks/purity": "warn",
91+
"react-hooks/refs": "warn",
92+
"react-hooks/preserve-manual-memoization": "warn",
93+
},
94+
},
95+
96+
// Vitest globals for tests — `vitest.config.js` sets `globals: true`,
97+
// so `describe` / `it` / `expect` / `vi` / `beforeEach` / `afterEach`
98+
// are available without import.
99+
{
100+
files: ["tests/**/*.{js,jsx}"],
101+
languageOptions: {
102+
globals: {
103+
...globals.browser,
104+
describe: "readonly",
105+
it: "readonly",
106+
test: "readonly",
107+
expect: "readonly",
108+
vi: "readonly",
109+
beforeEach: "readonly",
110+
afterEach: "readonly",
111+
beforeAll: "readonly",
112+
afterAll: "readonly",
113+
},
114+
},
115+
},
116+
]

frontend/src/hooks/useMotionAlerts.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ export function useMotionAlerts(cameras) {
2020
const { getToken } = useAuth()
2121
const { showToast } = useToasts()
2222
const abortRef = useRef(null)
23-
// Keep cameras ref current so the SSE callback always sees the latest map
23+
// Keep cameras ref current so the SSE callback always sees the latest
24+
// map. Updating the ref inside an effect (rather than during render)
25+
// satisfies React's purity rules — refs aren't supposed to be mutated
26+
// during render. The SSE callback fires asynchronously after commit so
27+
// it always reads the post-commit value.
2428
const camerasRef = useRef(cameras)
25-
camerasRef.current = cameras
29+
useEffect(() => {
30+
camerasRef.current = cameras
31+
}, [cameras])
2632

2733
useEffect(() => {
2834
let cancelled = false

0 commit comments

Comments
 (0)