diff --git a/fixtures/eslint-v10/README.md b/fixtures/eslint-v10/README.md new file mode 100644 index 000000000000..7b52e241f22b --- /dev/null +++ b/fixtures/eslint-v10/README.md @@ -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 +``` diff --git a/fixtures/eslint-v10/build.mjs b/fixtures/eslint-v10/build.mjs new file mode 100644 index 000000000000..e0dd355ba439 --- /dev/null +++ b/fixtures/eslint-v10/build.mjs @@ -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', +}); diff --git a/fixtures/eslint-v10/eslint.config.ts b/fixtures/eslint-v10/eslint.config.ts new file mode 100644 index 000000000000..f7d0bddac443 --- /dev/null +++ b/fixtures/eslint-v10/eslint.config.ts @@ -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', + }, + }, +]); diff --git a/fixtures/eslint-v10/index.js b/fixtures/eslint-v10/index.js new file mode 100644 index 000000000000..601b68a0328c --- /dev/null +++ b/fixtures/eslint-v10/index.js @@ -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 = () =>