@samitouri / QOS-React / commits / e02c173fa5

[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 ) {