Skip to content

Commit e8c6362

Browse files
authored
[eslint-plugin-react-hooks] Add ESLint v10 support (facebook#35720)
## Summary ESLint v10.0.0 was released on February 7, 2026. The current `peerDependencies` for `eslint-plugin-react-hooks` only allows up to `^9.0.0`, which causes peer dependency warnings when installing with ESLint v10. This PR: - Adds `^10.0.0` to the eslint peer dependency range - Adds `eslint-v10` to devDependencies for testing - Adds an `eslint-v10` e2e fixture (based on the existing `eslint-v9` fixture) ESLint v10's main breaking changes (removal of legacy eslintrc config, deprecated context methods) don't affect this plugin - flat config is already supported since v7.0.0, and the deprecated APIs already have fallbacks in place. ## How did you test this change? Ran the existing unit test suite: ``` cd packages/eslint-plugin-react-hooks && yarn test ``` All 5082 tests passed.
1 parent 03ca38e commit e8c6362

File tree

8 files changed

+265
-1
lines changed

8 files changed

+265
-1
lines changed

.github/workflows/runtime_eslint_plugin_e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
- "7"
3030
- "8"
3131
- "9"
32+
- "10"
3233
steps:
3334
- uses: actions/checkout@v4
3435
with:

fixtures/eslint-v10/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ESLint v10 Fixture
2+
3+
This fixture allows us to test e2e functionality for `eslint-plugin-react-hooks` with eslint version 10.
4+
5+
Run the following to test.
6+
7+
```sh
8+
cd fixtures/eslint-v10
9+
yarn
10+
yarn build
11+
yarn lint
12+
```

fixtures/eslint-v10/build.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
3+
import {execSync} from 'node:child_process';
4+
import {dirname, resolve} from 'node:path';
5+
import {fileURLToPath} from 'node:url';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = dirname(__filename);
9+
10+
execSync('yarn build -r stable eslint-plugin-react-hooks', {
11+
cwd: resolve(__dirname, '..', '..'),
12+
stdio: 'inherit',
13+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {defineConfig} from 'eslint/config';
2+
import reactHooks from 'eslint-plugin-react-hooks';
3+
4+
export default defineConfig([
5+
reactHooks.configs.flat['recommended-latest'],
6+
{
7+
languageOptions: {
8+
ecmaVersion: 'latest',
9+
sourceType: 'module',
10+
parserOptions: {
11+
ecmaFeatures: {
12+
jsx: true,
13+
},
14+
},
15+
},
16+
rules: {
17+
'react-hooks/exhaustive-deps': 'error',
18+
},
19+
},
20+
]);

fixtures/eslint-v10/index.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Exhaustive Deps
3+
*/
4+
// Valid because dependencies are declared correctly
5+
function Comment({comment, commentSource}) {
6+
const currentUserID = comment.viewer.id;
7+
const environment = RelayEnvironment.forUser(currentUserID);
8+
const commentID = nullthrows(comment.id);
9+
useEffect(() => {
10+
const subscription = SubscriptionCounter.subscribeOnce(
11+
`StoreSubscription_${commentID}`,
12+
() =>
13+
StoreSubscription.subscribe(
14+
environment,
15+
{
16+
comment_id: commentID,
17+
},
18+
currentUserID,
19+
commentSource
20+
)
21+
);
22+
return () => subscription.dispose();
23+
}, [commentID, commentSource, currentUserID, environment]);
24+
}
25+
26+
// Valid because no dependencies
27+
function UseEffectWithNoDependencies() {
28+
const local = {};
29+
useEffect(() => {
30+
console.log(local);
31+
});
32+
}
33+
function UseEffectWithEmptyDependencies() {
34+
useEffect(() => {
35+
const local = {};
36+
console.log(local);
37+
}, []);
38+
}
39+
40+
// OK because `props` wasn't defined.
41+
function ComponentWithNoPropsDefined() {
42+
useEffect(() => {
43+
console.log(props.foo);
44+
}, []);
45+
}
46+
47+
// Valid because props are declared as a dependency
48+
function ComponentWithPropsDeclaredAsDep({foo}) {
49+
useEffect(() => {
50+
console.log(foo.length);
51+
console.log(foo.slice(0));
52+
}, [foo]);
53+
}
54+
55+
// Valid because individual props are declared as dependencies
56+
function ComponentWithIndividualPropsDeclaredAsDeps(props) {
57+
useEffect(() => {
58+
console.log(props.foo);
59+
console.log(props.bar);
60+
}, [props.bar, props.foo]);
61+
}
62+
63+
// Invalid because neither props or props.foo are declared as dependencies
64+
function ComponentWithoutDeclaringPropAsDep(props) {
65+
useEffect(() => {
66+
console.log(props.foo);
67+
// eslint-disable-next-line react-hooks/exhaustive-deps
68+
}, []);
69+
useCallback(() => {
70+
console.log(props.foo);
71+
// eslint-disable-next-line react-hooks/exhaustive-deps
72+
}, []);
73+
// eslint-disable-next-line react-hooks/void-use-memo
74+
useMemo(() => {
75+
console.log(props.foo);
76+
// eslint-disable-next-line react-hooks/exhaustive-deps
77+
}, []);
78+
React.useEffect(() => {
79+
console.log(props.foo);
80+
// eslint-disable-next-line react-hooks/exhaustive-deps
81+
}, []);
82+
React.useCallback(() => {
83+
console.log(props.foo);
84+
// eslint-disable-next-line react-hooks/exhaustive-deps
85+
}, []);
86+
// eslint-disable-next-line react-hooks/void-use-memo
87+
React.useMemo(() => {
88+
console.log(props.foo);
89+
// eslint-disable-next-line react-hooks/exhaustive-deps
90+
}, []);
91+
React.notReactiveHook(() => {
92+
console.log(props.foo);
93+
}, []); // This one isn't a violation
94+
}
95+
96+
/**
97+
* Rules of Hooks
98+
*/
99+
// Valid because functions can call functions.
100+
function normalFunctionWithConditionalFunction() {
101+
if (cond) {
102+
doSomething();
103+
}
104+
}
105+
106+
// Valid because hooks can call hooks.
107+
function useHook() {
108+
useState();
109+
}
110+
const whatever = function useHook() {
111+
useState();
112+
};
113+
const useHook1 = () => {
114+
useState();
115+
};
116+
let useHook2 = () => useState();
117+
useHook2 = () => {
118+
useState();
119+
};
120+
121+
// Invalid because hooks can't be called in conditionals.
122+
function ComponentWithConditionalHook() {
123+
if (cond) {
124+
// eslint-disable-next-line react-hooks/rules-of-hooks
125+
useConditionalHook();
126+
}
127+
}
128+
129+
// Invalid because hooks can't be called in loops.
130+
function useHookInLoops() {
131+
while (a) {
132+
// eslint-disable-next-line react-hooks/rules-of-hooks
133+
useHook1();
134+
if (b) return;
135+
// eslint-disable-next-line react-hooks/rules-of-hooks
136+
useHook2();
137+
}
138+
while (c) {
139+
// eslint-disable-next-line react-hooks/rules-of-hooks
140+
useHook3();
141+
if (d) return;
142+
// eslint-disable-next-line react-hooks/rules-of-hooks
143+
useHook4();
144+
}
145+
}
146+
147+
/**
148+
* Compiler Rules
149+
*/
150+
// Invalid: component factory
151+
function InvalidComponentFactory() {
152+
const DynamicComponent = () => <div>Hello</div>;
153+
// eslint-disable-next-line react-hooks/static-components
154+
return <DynamicComponent />;
155+
}
156+
157+
// Invalid: mutating globals
158+
function InvalidGlobals() {
159+
// eslint-disable-next-line react-hooks/immutability
160+
window.myGlobal = 42;
161+
return <div>Done</div>;
162+
}
163+
164+
// Invalid: useMemo with wrong deps
165+
function InvalidUseMemo({items}) {
166+
// eslint-disable-next-line react-hooks/exhaustive-deps
167+
const sorted = useMemo(() => [...items].sort(), []);
168+
return <div>{sorted.length}</div>;
169+
}
170+
171+
// Invalid: missing/extra deps in useEffect
172+
function InvalidEffectDeps({a, b}) {
173+
useEffect(() => {
174+
console.log(a);
175+
// eslint-disable-next-line react-hooks/exhaustive-deps
176+
}, []);
177+
178+
useEffect(() => {
179+
console.log(a);
180+
// TODO: eslint-disable-next-line react-hooks/exhaustive-effect-dependencies
181+
}, [a, b]);
182+
}

fixtures/eslint-v10/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"private": true,
3+
"name": "eslint-v10",
4+
"dependencies": {
5+
"eslint": "^10.0.0",
6+
"eslint-plugin-react-hooks": "link:../../build/oss-stable/eslint-plugin-react-hooks",
7+
"jiti": "^2.4.2"
8+
},
9+
"scripts": {
10+
"build": "node build.mjs && yarn",
11+
"lint": "tsc --noEmit && eslint index.js --report-unused-disable-directives"
12+
},
13+
"devDependencies": {
14+
"typescript": "^5.4.3"
15+
}
16+
}

fixtures/eslint-v10/tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"lib": [
4+
"es2022"
5+
],
6+
"module": "nodenext",
7+
"moduleResolution": "nodenext",
8+
"target": "es2022",
9+
"typeRoots": [
10+
"./node_modules/@types"
11+
],
12+
"skipLibCheck": true
13+
},
14+
"exclude": [
15+
"node_modules",
16+
"**/node_modules",
17+
"../node_modules",
18+
"../../node_modules"
19+
]
20+
}

packages/eslint-plugin-react-hooks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
"homepage": "https://react.dev/",
3838
"peerDependencies": {
39-
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
39+
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
4040
},
4141
"dependencies": {
4242
"@babel/core": "^7.24.4",

0 commit comments

Comments
 (0)