[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