@samitouri / QOS-React / commits / 87a45ae37f

[eslint-plugin-react-hooks][RulesOfHooks] handle React.useEffect in addition to useEffect (#34076)

## Summary This is a fix for https://github.com/facebook/react/issues/34074 ## How did you test this change? I added tests in the eslint package, and ran `yarn jest`. After adding the new tests, I have this: On main | On this branch -|- <img width="356" height="88" alt="image" src="https://github.com/user-attachments/assets/4ae099a1-0156-4032-b2ca-635ebadcaa3f" /> | <img width="435" height="120" alt="image" src="https://github.com/user-attachments/assets/b06c04b8-6cec-43de-befa-a8b4dd20500e" /> ## Changes - Add tests to check that we are checking both `CallExpression` (`useEffect(`), and `MemberExpression` (`React.useEffect(`). To do that, I copied the `getNodeWithoutReactNamespace(` fn from `ExhaustiveDeps.ts` to `RulesOfHooks.ts`

Benjamin committed Aug 18, 2025 at 15:12 UTC 87a45ae37f4014b6df548a5d9b06bad5dc557992
3 files changed +68 -3
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+32
@@ -7735,6 +7735,9 @@ if (__EXPERIMENTAL__) {
7735 useEffect(() => {
7736 onStuff();
7737 }, []);
7738 + React.useEffect(() => {
7739 + onStuff();
7740 + }, []);
7741 }
7742 `,
7743 },
@@ -7751,6 +7754,9 @@ if (__EXPERIMENTAL__) {
7754 useEffect(() => {
7755 onStuff();
7756 }, [onStuff]);
7757 + React.useEffect(() => {
7758 + onStuff();
7759 + }, [onStuff]);
7760 }
7761 `,
7762 errors: [
@@ -7769,6 +7775,32 @@ if (__EXPERIMENTAL__) {
7775 useEffect(() => {
7776 onStuff();
7777 }, []);
7778 + React.useEffect(() => {
7779 + onStuff();
7780 + }, [onStuff]);
7781 + }
7782 + `,
7783 + },
7784 + ],
7785 + },
7786 + {
7787 + message:
7788 + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
7789 + 'Remove `onStuff` from the list.',
7790 + suggestions: [
7791 + {
7792 + desc: 'Remove the dependency `onStuff`',
7793 + output: normalizeIndent`
7794 + function MyComponent({ theme }) {
7795 + const onStuff = useEffectEvent(() => {
7796 + showNotification(theme);
7797 + });
7798 + useEffect(() => {
7799 + onStuff();
7800 + }, [onStuff]);
7801 + React.useEffect(() => {
7802 + onStuff();
7803 + }, []);
7804 }
7805 `,
7806 },
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+11
@@ -1368,6 +1368,9 @@ if (__EXPERIMENTAL__) {
1368 useEffect(() => {
1369 onClick();
1370 });
1371 + React.useEffect(() => {
1372 + onClick();
1373 + });
1374 }
1375 `,
1376 },
@@ -1389,6 +1392,10 @@ if (__EXPERIMENTAL__) {
1392 let id = setInterval(() => onClick(), 100);
1393 return () => clearInterval(onClick);
1394 }, []);
1395 + React.useEffect(() => {
1396 + let id = setInterval(() => onClick(), 100);
1397 + return () => clearInterval(onClick);
1398 + }, []);
1399 return null;
1400 }
1401 `,
@@ -1408,6 +1415,7 @@ if (__EXPERIMENTAL__) {
1415 {
1416 code: normalizeIndent`
1417 function MyComponent({ theme }) {
1418 + // Can receive arguments
1419 const onEvent = useEffectEvent((text) => {
1420 console.log(text);
1421 });
@@ -1415,6 +1423,9 @@ if (__EXPERIMENTAL__) {
1423 useEffect(() => {
1424 onEvent('Hello world');
1425 });
1426 + React.useEffect(() => {
1427 + onEvent('Hello world');
1428 + });
1429 }
1430 `,
1431 },
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+25 -3
@@ -11,7 +11,10 @@ import type {
11 CallExpression,
12 CatchClause,
13 DoWhileStatement,
14 + Expression,
15 + Identifier,
16 Node,
17 + Super,
18 TryStatement,
19 } from 'estree';
20
@@ -129,6 +132,24 @@ function isInsideTryCatch(
132 return false;
133 }
134
135 +function getNodeWithoutReactNamespace(
136 + node: Expression | Super,
137 +): Expression | Identifier | Super {
138 + if (
139 + node.type === 'MemberExpression' &&
140 + node.object.type === 'Identifier' &&
141 + node.object.name === 'React' &&
142 + node.property.type === 'Identifier' &&
143 + !node.computed
144 + ) {
145 + return node.property;
146 + }
147 + return node;
148 +}
149 +
150 +function isUseEffectIdentifier(node: Node): boolean {
151 + return node.type === 'Identifier' && node.name === 'useEffect';
152 +}
153 function isUseEffectEventIdentifier(node: Node): boolean {
154 if (__EXPERIMENTAL__) {
155 return node.type === 'Identifier' && node.name === 'useEffectEvent';
@@ -702,10 +723,11 @@ const rule = {
723
724 // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in
725 // another useEffectEvent
726 + // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
727 + const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
728 if (
706 - node.callee.type === 'Identifier' &&
707 - (node.callee.name === 'useEffect' ||
708 - isUseEffectEventIdentifier(node.callee)) &&
729 + (isUseEffectIdentifier(nodeWithoutNamespace) ||
730 + isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
731 node.arguments.length > 0
732 ) {
733 // Denote that we have traversed into a useEffect call, and stash the CallExpr for