mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +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"
|
- "7"
|
||||||
- "8"
|
- "8"
|
||||||
- "9"
|
- "9"
|
||||||
|
- "10"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
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/",
|
"homepage": "https://react.dev/",
|
||||||
"peerDependencies": {
|
"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": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.24.4",
|
"@babel/core": "^7.24.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user