[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.
This commit is contained in:
Azat S.
2026-02-13 21:26:01 +03:00
committed by GitHub
parent 03ca38e6e7
commit e8c6362678
8 changed files with 265 additions and 1 deletions

View File

@@ -29,6 +29,7 @@ jobs:
- "7"
- "8"
- "9"
- "10"
steps:
- uses: actions/checkout@v4
with:

View File

@@ -0,0 +1,12 @@
# ESLint v10 Fixture
This fixture allows us to test e2e functionality for `eslint-plugin-react-hooks` with eslint version 10.
Run the following to test.
```sh
cd fixtures/eslint-v10
yarn
yarn build
yarn lint
```

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env node
import {execSync} from 'node:child_process';
import {dirname, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
execSync('yarn build -r stable eslint-plugin-react-hooks', {
cwd: resolve(__dirname, '..', '..'),
stdio: 'inherit',
});

View File

@@ -0,0 +1,20 @@
import {defineConfig} from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';
export default defineConfig([
reactHooks.configs.flat['recommended-latest'],
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
'react-hooks/exhaustive-deps': 'error',
},
},
]);

View File

@@ -0,0 +1,182 @@
/**
* Exhaustive Deps
*/
// Valid because dependencies are declared correctly
function Comment({comment, commentSource}) {
const currentUserID = comment.viewer.id;
const environment = RelayEnvironment.forUser(currentUserID);
const commentID = nullthrows(comment.id);
useEffect(() => {
const subscription = SubscriptionCounter.subscribeOnce(
`StoreSubscription_${commentID}`,
() =>
StoreSubscription.subscribe(
environment,
{
comment_id: commentID,
},
currentUserID,
commentSource
)
);
return () => subscription.dispose();
}, [commentID, commentSource, currentUserID, environment]);
}
// Valid because no dependencies
function UseEffectWithNoDependencies() {
const local = {};
useEffect(() => {
console.log(local);
});
}
function UseEffectWithEmptyDependencies() {
useEffect(() => {
const local = {};
console.log(local);
}, []);
}
// OK because `props` wasn't defined.
function ComponentWithNoPropsDefined() {
useEffect(() => {
console.log(props.foo);
}, []);
}
// Valid because props are declared as a dependency
function ComponentWithPropsDeclaredAsDep({foo}) {
useEffect(() => {
console.log(foo.length);
console.log(foo.slice(0));
}, [foo]);
}
// Valid because individual props are declared as dependencies
function ComponentWithIndividualPropsDeclaredAsDeps(props) {
useEffect(() => {
console.log(props.foo);
console.log(props.bar);
}, [props.bar, props.foo]);
}
// Invalid because neither props or props.foo are declared as dependencies
function ComponentWithoutDeclaringPropAsDep(props) {
useEffect(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useCallback(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line react-hooks/void-use-memo
useMemo(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useCallback(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line react-hooks/void-use-memo
React.useMemo(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.notReactiveHook(() => {
console.log(props.foo);
}, []); // This one isn't a violation
}
/**
* Rules of Hooks
*/
// Valid because functions can call functions.
function normalFunctionWithConditionalFunction() {
if (cond) {
doSomething();
}
}
// Valid because hooks can call hooks.
function useHook() {
useState();
}
const whatever = function useHook() {
useState();
};
const useHook1 = () => {
useState();
};
let useHook2 = () => useState();
useHook2 = () => {
useState();
};
// Invalid because hooks can't be called in conditionals.
function ComponentWithConditionalHook() {
if (cond) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useConditionalHook();
}
}
// Invalid because hooks can't be called in loops.
function useHookInLoops() {
while (a) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook1();
if (b) return;
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook2();
}
while (c) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook3();
if (d) return;
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}
// Invalid: missing/extra deps in useEffect
function InvalidEffectDeps({a, b}) {
useEffect(() => {
console.log(a);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
console.log(a);
// TODO: eslint-disable-next-line react-hooks/exhaustive-effect-dependencies
}, [a, b]);
}

View File

@@ -0,0 +1,16 @@
{
"private": true,
"name": "eslint-v10",
"dependencies": {
"eslint": "^10.0.0",
"eslint-plugin-react-hooks": "link:../../build/oss-stable/eslint-plugin-react-hooks",
"jiti": "^2.4.2"
},
"scripts": {
"build": "node build.mjs && yarn",
"lint": "tsc --noEmit && eslint index.js --report-unused-disable-directives"
},
"devDependencies": {
"typescript": "^5.4.3"
}
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"lib": [
"es2022"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"typeRoots": [
"./node_modules/@types"
],
"skipLibCheck": true
},
"exclude": [
"node_modules",
"**/node_modules",
"../node_modules",
"../../node_modules"
]
}

View File

@@ -36,7 +36,7 @@
},
"homepage": "https://react.dev/",
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
"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"
},
"dependencies": {
"@babel/core": "^7.24.4",