Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions fixtures/eslint-v10/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions fixtures/eslint-v10/build.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions fixtures/eslint-v10/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 182 additions & 0 deletions fixtures/eslint-v10/index.js
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions fixtures/eslint-v10/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions fixtures/eslint-v10/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
2 changes: 1 addition & 1 deletion packages/eslint-plugin-react-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down