mirror of
https://github.com/facebook/react.git
synced 2026-02-21 19:31:52 +00:00
[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:
@@ -29,6 +29,7 @@ jobs:
|
||||
- "7"
|
||||
- "8"
|
||||
- "9"
|
||||
- "10"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
12
fixtures/eslint-v10/README.md
Normal file
12
fixtures/eslint-v10/README.md
Normal 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
|
||||
```
|
||||
13
fixtures/eslint-v10/build.mjs
Normal file
13
fixtures/eslint-v10/build.mjs
Normal 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',
|
||||
});
|
||||
20
fixtures/eslint-v10/eslint.config.ts
Normal file
20
fixtures/eslint-v10/eslint.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
]);
|
||||
182
fixtures/eslint-v10/index.js
Normal file
182
fixtures/eslint-v10/index.js
Normal 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]);
|
||||
}
|
||||
16
fixtures/eslint-v10/package.json
Normal file
16
fixtures/eslint-v10/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
fixtures/eslint-v10/tsconfig.json
Normal file
20
fixtures/eslint-v10/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user