@samitouri / QOS-React-1 / commits / e8c6362678

[eslint-plugin-react-hooks] Add ESLint v10 support (#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.

Azat S. committed Feb 13, 2026 at 21:26 UTC e8c6362678c8bc86a02b8444d2c3f597b3dc4e22
8 files changed +265 -1
.github/workflows/runtime_eslint_plugin_e2e.yml
+1
@@ -29,6 +29,7 @@ jobs:
29 - "7"
30 - "8"
31 - "9"
32 + - "10"
33 steps:
34 - uses: actions/checkout@v4
35 with:
fixtures/eslint-v10/README.md new
+12
@@ -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 new
+13
@@ -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 +});
fixtures/eslint-v10/eslint.config.ts new
+20
@@ -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 new
+182
@@ -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 new
+16
@@ -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 new
+20
@@ -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
+1 -1
@@ -36,7 +36,7 @@
36 },
37 "homepage": "https://react.dev/",
38 "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"
40 },
41 "dependencies": {
42 "@babel/core": "^7.24.4",