From 0d0e0c6ae1c392648a25cb3e61f9379a7765d737 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Tue, 30 Sep 2025 16:17:31 -0400 Subject: [PATCH] [lint] Remove experimental gating for useEffectEvent rules --- .../ESLintRuleExhaustiveDeps-test.js | 44 ++- .../__tests__/ESLintRulesOfHooks-test.js | 287 +++++++++--------- .../src/rules/ExhaustiveDeps.ts | 5 +- .../src/rules/RulesOfHooks.ts | 10 +- 4 files changed, 161 insertions(+), 185 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index dca94c516c296..b479ce48521ca 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -1549,6 +1549,21 @@ const tests = { }, }, }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, []); + React.useEffect(() => { + onStuff(); + }, []); + } + `, + }, ], invalid: [ { @@ -7819,31 +7834,6 @@ const tests = { }, ], }, - ], -}; - -if (__EXPERIMENTAL__) { - tests.valid = [ - ...tests.valid, - { - code: normalizeIndent` - function MyComponent({ theme }) { - const onStuff = useEffectEvent(() => { - showNotification(theme); - }); - useEffect(() => { - onStuff(); - }, []); - React.useEffect(() => { - onStuff(); - }, []); - } - `, - }, - ]; - - tests.invalid = [ - ...tests.invalid, { code: normalizeIndent` function MyComponent({ theme }) { @@ -7907,8 +7897,8 @@ if (__EXPERIMENTAL__) { }, ], }, - ]; -} + ], +}; // Tests that are only valid/invalid across parsers supporting Flow const testsFlow = { diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 83455f0b8d43f..bfde0e69e16a6 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -602,6 +602,143 @@ const allTests = { }, }, }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in a useEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onClick(); + }); + React.useEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be passed by reference in useEffect + // and useEffectEvent. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = useEffectEvent(() => { + debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); + }); + useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; + } + `, + }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + useEffect(() => { + onClick(); + }); + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + } + `, + }, + { + code: normalizeIndent` + function MyComponent({ theme }) { + // Can receive arguments + const onEvent = useEffectEvent((text) => { + console.log(text); + }); + + useEffect(() => { + onEvent('Hello world'); + }); + React.useEffect(() => { + onEvent('Hello world'); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useLayoutEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useLayoutEffect(() => { + onClick(); + }); + React.useLayoutEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useInsertionEffect(() => { + onClick(); + }); + React.useInsertionEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect + // and useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = useEffectEvent(() => { + debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); + }); + useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; + } + `, + }, ], invalid: [ { @@ -1407,152 +1544,6 @@ const allTests = { }, errors: [useEffectEventError('onClick', true)], }, - ], -}; - -if (__EXPERIMENTAL__) { - allTests.valid = [ - ...allTests.valid, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in a useEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useEffect(() => { - onClick(); - }); - React.useEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be passed by reference in useEffect - // and useEffectEvent. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - const onClick2 = useEffectEvent(() => { - debounce(onClick); - debounce(() => onClick()); - debounce(() => { onClick() }); - deboucne(() => debounce(onClick)); - }); - useEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - return null; - } - `, - }, - { - code: normalizeIndent` - function MyComponent({ theme }) { - useEffect(() => { - onClick(); - }); - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - } - `, - }, - { - code: normalizeIndent` - function MyComponent({ theme }) { - // Can receive arguments - const onEvent = useEffectEvent((text) => { - console.log(text); - }); - - useEffect(() => { - onEvent('Hello world'); - }); - React.useEffect(() => { - onEvent('Hello world'); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in useLayoutEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useLayoutEffect(() => { - onClick(); - }); - React.useLayoutEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in useInsertionEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - useInsertionEffect(() => { - onClick(); - }); - React.useInsertionEffect(() => { - onClick(); - }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect - // and useInsertionEffect. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - const onClick2 = useEffectEvent(() => { - debounce(onClick); - debounce(() => onClick()); - debounce(() => { onClick() }); - deboucne(() => debounce(onClick)); - }); - useLayoutEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useLayoutEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - useInsertionEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - React.useInsertionEffect(() => { - let id = setInterval(() => onClick(), 100); - return () => clearInterval(onClick); - }, []); - return null; - } - `, - }, - ]; - allTests.invalid = [ - ...allTests.invalid, { code: normalizeIndent` function MyComponent({ theme }) { @@ -1659,8 +1650,8 @@ if (__EXPERIMENTAL__) { useEffectEventError('onClick', true), ], }, - ]; -} + ], +}; function conditionalError(hook, hasPreviousFinalizer = false) { return { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 8523c3cc2eca8..05321ffb46f6e 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -2122,10 +2122,7 @@ function isAncestorNodeOf(a: Node, b: Node): boolean { } function isUseEffectEventIdentifier(node: Node): boolean { - if (__EXPERIMENTAL__) { - return node.type === 'Identifier' && node.name === 'useEffectEvent'; - } - return false; + return node.type === 'Identifier' && node.name === 'useEffectEvent'; } function getUnknownDependenciesMessage(reactiveHookName: string): string { diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 97909d6b0f80c..cb89bbfea9c49 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -20,7 +20,7 @@ import type { // @ts-expect-error untyped module import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; -import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; +import {getAdditionalEffectHooksFromSettings} from '../shared/Utils'; /** * Catch all identifiers that begin with "use" followed by an uppercase Latin @@ -167,10 +167,7 @@ function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean { return false; } function isUseEffectEventIdentifier(node: Node): boolean { - if (__EXPERIMENTAL__) { - return node.type === 'Identifier' && node.name === 'useEffectEvent'; - } - return false; + return node.type === 'Identifier' && node.name === 'useEffectEvent'; } function isUseIdentifier(node: Node): boolean { @@ -200,7 +197,8 @@ const rule = { create(context: Rule.RuleContext) { const settings = context.settings || {}; - const additionalEffectHooks = getAdditionalEffectHooksFromSettings(settings); + const additionalEffectHooks = + getAdditionalEffectHooksFromSettings(settings); let lastEffect: CallExpression | null = null; const codePathReactHooksMapStack: Array<