[lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect (#34492)
--- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34492). * #34497 * __->__ #34492
Jordan Brown committed
Sep 23, 2025 at 16:56 UTC
e02c173fa597ce914c6876f71556c39bf20f40dc
2 files changed
+69
-3
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+66
@@ -1430,6 +1430,72 @@ if (__EXPERIMENTAL__) {
1430
}
1431
`,
1432
},
1433
+ {
1434
+ code: normalizeIndent`
1435
+ // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
1436
+ function MyComponent({ theme }) {
1437
+ const onClick = useEffectEvent(() => {
1438
+ showNotification(theme);
1439
+ });
1440
+ useLayoutEffect(() => {
1441
+ onClick();
1442
+ });
1443
+ React.useLayoutEffect(() => {
1444
+ onClick();
1445
+ });
1446
+ }
1447
+ `,
1448
+ },
1449
+ {
1450
+ code: normalizeIndent`
1451
+ // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
1452
+ function MyComponent({ theme }) {
1453
+ const onClick = useEffectEvent(() => {
1454
+ showNotification(theme);
1455
+ });
1456
+ useInsertionEffect(() => {
1457
+ onClick();
1458
+ });
1459
+ React.useInsertionEffect(() => {
1460
+ onClick();
1461
+ });
1462
+ }
1463
+ `,
1464
+ },
1465
+ {
1466
+ code: normalizeIndent`
1467
+ // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
1468
+ // and useInsertionEffect.
1469
+ function MyComponent({ theme }) {
1470
+ const onClick = useEffectEvent(() => {
1471
+ showNotification(theme);
1472
+ });
1473
+ const onClick2 = useEffectEvent(() => {
1474
+ debounce(onClick);
1475
+ debounce(() => onClick());
1476
+ debounce(() => { onClick() });
1477
+ deboucne(() => debounce(onClick));
1478
+ });
1479
+ useLayoutEffect(() => {
1480
+ let id = setInterval(() => onClick(), 100);
1481
+ return () => clearInterval(onClick);
1482
+ }, []);
1483
+ React.useLayoutEffect(() => {
1484
+ let id = setInterval(() => onClick(), 100);
1485
+ return () => clearInterval(onClick);
1486
+ }, []);
1487
+ useInsertionEffect(() => {
1488
+ let id = setInterval(() => onClick(), 100);
1489
+ return () => clearInterval(onClick);
1490
+ }, []);
1491
+ React.useInsertionEffect(() => {
1492
+ let id = setInterval(() => onClick(), 100);
1493
+ return () => clearInterval(onClick);
1494
+ }, []);
1495
+ return null;
1496
+ }
1497
+ `,
1498
+ },
1499
];
1500
allTests.invalid = [
1501
...allTests.invalid,
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+3
-3
@@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace(
147
return node;
148
}
149
150
-function isUseEffectIdentifier(node: Node): boolean {
151
- return node.type === 'Identifier' && node.name === 'useEffect';
150
+function isEffectIdentifier(node: Node): boolean {
151
+ return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
152
}
153
function isUseEffectEventIdentifier(node: Node): boolean {
154
if (__EXPERIMENTAL__) {
@@ -726,7 +726,7 @@ const rule = {
726
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
727
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
728
if (
729
- (isUseEffectIdentifier(nodeWithoutNamespace) ||
729
+ (isEffectIdentifier(nodeWithoutNamespace) ||
730
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
731
node.arguments.length > 0
732
) {