diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 555c54148ec1e..7a6b09c5440c4 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -7735,6 +7735,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, []); + React.useEffect(() => { + onStuff(); + }, []); } `, }, @@ -7751,6 +7754,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, [onStuff]); + React.useEffect(() => { + onStuff(); + }, [onStuff]); } `, errors: [ @@ -7769,6 +7775,32 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, []); + React.useEffect(() => { + onStuff(); + }, [onStuff]); + } + `, + }, + ], + }, + { + message: + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + + 'Remove `onStuff` from the list.', + suggestions: [ + { + desc: 'Remove the dependency `onStuff`', + output: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, [onStuff]); + React.useEffect(() => { + onStuff(); + }, []); } `, }, diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 7cb3ef0495341..ac8886c776802 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1368,6 +1368,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onClick(); }); + React.useEffect(() => { + onClick(); + }); } `, }, @@ -1389,6 +1392,10 @@ if (__EXPERIMENTAL__) { let id = setInterval(() => onClick(), 100); return () => clearInterval(onClick); }, []); + React.useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); return null; } `, @@ -1408,6 +1415,7 @@ if (__EXPERIMENTAL__) { { code: normalizeIndent` function MyComponent({ theme }) { + // Can receive arguments const onEvent = useEffectEvent((text) => { console.log(text); }); @@ -1415,6 +1423,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onEvent('Hello world'); }); + React.useEffect(() => { + onEvent('Hello world'); + }); } `, }, diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index f0a2ffbda9e18..0721a75e00642 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -11,7 +11,10 @@ import type { CallExpression, CatchClause, DoWhileStatement, + Expression, + Identifier, Node, + Super, TryStatement, } from 'estree'; @@ -129,6 +132,24 @@ function isInsideTryCatch( return false; } +function getNodeWithoutReactNamespace( + node: Expression | Super, +): Expression | Identifier | Super { + if ( + node.type === 'MemberExpression' && + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' && + !node.computed + ) { + return node.property; + } + return node; +} + +function isUseEffectIdentifier(node: Node): boolean { + return node.type === 'Identifier' && node.name === 'useEffect'; +} function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { return node.type === 'Identifier' && node.name === 'useEffectEvent'; @@ -702,10 +723,11 @@ const rule = { // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in // another useEffectEvent + // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent` + const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee); if ( - node.callee.type === 'Identifier' && - (node.callee.name === 'useEffect' || - isUseEffectEventIdentifier(node.callee)) && + (isUseEffectIdentifier(nodeWithoutNamespace) || + isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0 ) { // Denote that we have traversed into a useEffect call, and stash the CallExpr for