[lint] Remove experimental gating useEffectEvent rules (#34660)
Stacked on https://github.com/facebook/react/pull/34637 `useEffectEvent` is now in canary so we need to remove this `__EXPERIMENTAL__` gating on the rules and tests
Jack Pope committed
Sep 30, 2025 at 16:55 UTC
57b16e37887b324e4a077fd302b805d63421b695
4 files changed
+161
-185
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+17
-27
@@ -1549,6 +1549,21 @@ const tests = {
1549
},
1550
},
1551
},
1552
+ {
1553
+ code: normalizeIndent`
1554
+ function MyComponent({ theme }) {
1555
+ const onStuff = useEffectEvent(() => {
1556
+ showNotification(theme);
1557
+ });
1558
+ useEffect(() => {
1559
+ onStuff();
1560
+ }, []);
1561
+ React.useEffect(() => {
1562
+ onStuff();
1563
+ }, []);
1564
+ }
1565
+ `,
1566
+ },
1567
],
1568
invalid: [
1569
{
@@ -7819,31 +7834,6 @@ const tests = {
7834
},
7835
],
7836
},
7822
- ],
7823
-};
7824
-
7825
-if (__EXPERIMENTAL__) {
7826
- tests.valid = [
7827
- ...tests.valid,
7828
- {
7829
- code: normalizeIndent`
7830
- function MyComponent({ theme }) {
7831
- const onStuff = useEffectEvent(() => {
7832
- showNotification(theme);
7833
- });
7834
- useEffect(() => {
7835
- onStuff();
7836
- }, []);
7837
- React.useEffect(() => {
7838
- onStuff();
7839
- }, []);
7840
- }
7841
- `,
7842
- },
7843
- ];
7844
-
7845
- tests.invalid = [
7846
- ...tests.invalid,
7837
{
7838
code: normalizeIndent`
7839
function MyComponent({ theme }) {
@@ -7907,8 +7897,8 @@ if (__EXPERIMENTAL__) {
7897
},
7898
],
7899
},
7910
- ];
7911
-}
7900
+ ],
7901
+};
7902
7903
// Tests that are only valid/invalid across parsers supporting Flow
7904
const testsFlow = {
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+139
-148
@@ -602,6 +602,143 @@ const allTests = {
602
},
603
},
604
},
605
+ {
606
+ code: normalizeIndent`
607
+ // Valid because functions created with useEffectEvent can be called in a useEffect.
608
+ function MyComponent({ theme }) {
609
+ const onClick = useEffectEvent(() => {
610
+ showNotification(theme);
611
+ });
612
+ useEffect(() => {
613
+ onClick();
614
+ });
615
+ React.useEffect(() => {
616
+ onClick();
617
+ });
618
+ }
619
+ `,
620
+ },
621
+ {
622
+ code: normalizeIndent`
623
+ // Valid because functions created with useEffectEvent can be passed by reference in useEffect
624
+ // and useEffectEvent.
625
+ function MyComponent({ theme }) {
626
+ const onClick = useEffectEvent(() => {
627
+ showNotification(theme);
628
+ });
629
+ const onClick2 = useEffectEvent(() => {
630
+ debounce(onClick);
631
+ debounce(() => onClick());
632
+ debounce(() => { onClick() });
633
+ deboucne(() => debounce(onClick));
634
+ });
635
+ useEffect(() => {
636
+ let id = setInterval(() => onClick(), 100);
637
+ return () => clearInterval(onClick);
638
+ }, []);
639
+ React.useEffect(() => {
640
+ let id = setInterval(() => onClick(), 100);
641
+ return () => clearInterval(onClick);
642
+ }, []);
643
+ return null;
644
+ }
645
+ `,
646
+ },
647
+ {
648
+ code: normalizeIndent`
649
+ function MyComponent({ theme }) {
650
+ useEffect(() => {
651
+ onClick();
652
+ });
653
+ const onClick = useEffectEvent(() => {
654
+ showNotification(theme);
655
+ });
656
+ }
657
+ `,
658
+ },
659
+ {
660
+ code: normalizeIndent`
661
+ function MyComponent({ theme }) {
662
+ // Can receive arguments
663
+ const onEvent = useEffectEvent((text) => {
664
+ console.log(text);
665
+ });
666
+
667
+ useEffect(() => {
668
+ onEvent('Hello world');
669
+ });
670
+ React.useEffect(() => {
671
+ onEvent('Hello world');
672
+ });
673
+ }
674
+ `,
675
+ },
676
+ {
677
+ code: normalizeIndent`
678
+ // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
679
+ function MyComponent({ theme }) {
680
+ const onClick = useEffectEvent(() => {
681
+ showNotification(theme);
682
+ });
683
+ useLayoutEffect(() => {
684
+ onClick();
685
+ });
686
+ React.useLayoutEffect(() => {
687
+ onClick();
688
+ });
689
+ }
690
+ `,
691
+ },
692
+ {
693
+ code: normalizeIndent`
694
+ // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
695
+ function MyComponent({ theme }) {
696
+ const onClick = useEffectEvent(() => {
697
+ showNotification(theme);
698
+ });
699
+ useInsertionEffect(() => {
700
+ onClick();
701
+ });
702
+ React.useInsertionEffect(() => {
703
+ onClick();
704
+ });
705
+ }
706
+ `,
707
+ },
708
+ {
709
+ code: normalizeIndent`
710
+ // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
711
+ // and useInsertionEffect.
712
+ function MyComponent({ theme }) {
713
+ const onClick = useEffectEvent(() => {
714
+ showNotification(theme);
715
+ });
716
+ const onClick2 = useEffectEvent(() => {
717
+ debounce(onClick);
718
+ debounce(() => onClick());
719
+ debounce(() => { onClick() });
720
+ deboucne(() => debounce(onClick));
721
+ });
722
+ useLayoutEffect(() => {
723
+ let id = setInterval(() => onClick(), 100);
724
+ return () => clearInterval(onClick);
725
+ }, []);
726
+ React.useLayoutEffect(() => {
727
+ let id = setInterval(() => onClick(), 100);
728
+ return () => clearInterval(onClick);
729
+ }, []);
730
+ useInsertionEffect(() => {
731
+ let id = setInterval(() => onClick(), 100);
732
+ return () => clearInterval(onClick);
733
+ }, []);
734
+ React.useInsertionEffect(() => {
735
+ let id = setInterval(() => onClick(), 100);
736
+ return () => clearInterval(onClick);
737
+ }, []);
738
+ return null;
739
+ }
740
+ `,
741
+ },
742
],
743
invalid: [
744
{
@@ -1407,152 +1544,6 @@ const allTests = {
1544
},
1545
errors: [useEffectEventError('onClick', true)],
1546
},
1410
- ],
1411
-};
1412
-
1413
-if (__EXPERIMENTAL__) {
1414
- allTests.valid = [
1415
- ...allTests.valid,
1416
- {
1417
- code: normalizeIndent`
1418
- // Valid because functions created with useEffectEvent can be called in a useEffect.
1419
- function MyComponent({ theme }) {
1420
- const onClick = useEffectEvent(() => {
1421
- showNotification(theme);
1422
- });
1423
- useEffect(() => {
1424
- onClick();
1425
- });
1426
- React.useEffect(() => {
1427
- onClick();
1428
- });
1429
- }
1430
- `,
1431
- },
1432
- {
1433
- code: normalizeIndent`
1434
- // Valid because functions created with useEffectEvent can be passed by reference in useEffect
1435
- // and useEffectEvent.
1436
- function MyComponent({ theme }) {
1437
- const onClick = useEffectEvent(() => {
1438
- showNotification(theme);
1439
- });
1440
- const onClick2 = useEffectEvent(() => {
1441
- debounce(onClick);
1442
- debounce(() => onClick());
1443
- debounce(() => { onClick() });
1444
- deboucne(() => debounce(onClick));
1445
- });
1446
- useEffect(() => {
1447
- let id = setInterval(() => onClick(), 100);
1448
- return () => clearInterval(onClick);
1449
- }, []);
1450
- React.useEffect(() => {
1451
- let id = setInterval(() => onClick(), 100);
1452
- return () => clearInterval(onClick);
1453
- }, []);
1454
- return null;
1455
- }
1456
- `,
1457
- },
1458
- {
1459
- code: normalizeIndent`
1460
- function MyComponent({ theme }) {
1461
- useEffect(() => {
1462
- onClick();
1463
- });
1464
- const onClick = useEffectEvent(() => {
1465
- showNotification(theme);
1466
- });
1467
- }
1468
- `,
1469
- },
1470
- {
1471
- code: normalizeIndent`
1472
- function MyComponent({ theme }) {
1473
- // Can receive arguments
1474
- const onEvent = useEffectEvent((text) => {
1475
- console.log(text);
1476
- });
1477
-
1478
- useEffect(() => {
1479
- onEvent('Hello world');
1480
- });
1481
- React.useEffect(() => {
1482
- onEvent('Hello world');
1483
- });
1484
- }
1485
- `,
1486
- },
1487
- {
1488
- code: normalizeIndent`
1489
- // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
1490
- function MyComponent({ theme }) {
1491
- const onClick = useEffectEvent(() => {
1492
- showNotification(theme);
1493
- });
1494
- useLayoutEffect(() => {
1495
- onClick();
1496
- });
1497
- React.useLayoutEffect(() => {
1498
- onClick();
1499
- });
1500
- }
1501
- `,
1502
- },
1503
- {
1504
- code: normalizeIndent`
1505
- // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
1506
- function MyComponent({ theme }) {
1507
- const onClick = useEffectEvent(() => {
1508
- showNotification(theme);
1509
- });
1510
- useInsertionEffect(() => {
1511
- onClick();
1512
- });
1513
- React.useInsertionEffect(() => {
1514
- onClick();
1515
- });
1516
- }
1517
- `,
1518
- },
1519
- {
1520
- code: normalizeIndent`
1521
- // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
1522
- // and useInsertionEffect.
1523
- function MyComponent({ theme }) {
1524
- const onClick = useEffectEvent(() => {
1525
- showNotification(theme);
1526
- });
1527
- const onClick2 = useEffectEvent(() => {
1528
- debounce(onClick);
1529
- debounce(() => onClick());
1530
- debounce(() => { onClick() });
1531
- deboucne(() => debounce(onClick));
1532
- });
1533
- useLayoutEffect(() => {
1534
- let id = setInterval(() => onClick(), 100);
1535
- return () => clearInterval(onClick);
1536
- }, []);
1537
- React.useLayoutEffect(() => {
1538
- let id = setInterval(() => onClick(), 100);
1539
- return () => clearInterval(onClick);
1540
- }, []);
1541
- useInsertionEffect(() => {
1542
- let id = setInterval(() => onClick(), 100);
1543
- return () => clearInterval(onClick);
1544
- }, []);
1545
- React.useInsertionEffect(() => {
1546
- let id = setInterval(() => onClick(), 100);
1547
- return () => clearInterval(onClick);
1548
- }, []);
1549
- return null;
1550
- }
1551
- `,
1552
- },
1553
- ];
1554
- allTests.invalid = [
1555
- ...allTests.invalid,
1547
{
1548
code: normalizeIndent`
1549
function MyComponent({ theme }) {
@@ -1659,8 +1650,8 @@ if (__EXPERIMENTAL__) {
1650
useEffectEventError('onClick', true),
1651
],
1652
},
1662
- ];
1663
-}
1653
+ ],
1654
+};
1655
1656
function conditionalError(hook, hasPreviousFinalizer = false) {
1657
return {
packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts
+1
-4
@@ -2122,10 +2122,7 @@ function isAncestorNodeOf(a: Node, b: Node): boolean {
2122
}
2123
2124
function isUseEffectEventIdentifier(node: Node): boolean {
2125
- if (__EXPERIMENTAL__) {
2126
- return node.type === 'Identifier' && node.name === 'useEffectEvent';
2127
- }
2128
- return false;
2125
+ return node.type === 'Identifier' && node.name === 'useEffectEvent';
2126
}
2127
2128
function getUnknownDependenciesMessage(reactiveHookName: string): string {
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+4
-6
@@ -20,7 +20,7 @@ import type {
20
21
// @ts-expect-error untyped module
22
import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';
23
-import { getAdditionalEffectHooksFromSettings } from '../shared/Utils';
23
+import {getAdditionalEffectHooksFromSettings} from '../shared/Utils';
24
25
/**
26
* Catch all identifiers that begin with "use" followed by an uppercase Latin
@@ -167,10 +167,7 @@ function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {
167
return false;
168
}
169
function isUseEffectEventIdentifier(node: Node): boolean {
170
- if (__EXPERIMENTAL__) {
171
- return node.type === 'Identifier' && node.name === 'useEffectEvent';
172
- }
173
- return false;
170
+ return node.type === 'Identifier' && node.name === 'useEffectEvent';
171
}
172
173
function isUseIdentifier(node: Node): boolean {
@@ -200,7 +197,8 @@ const rule = {
197
create(context: Rule.RuleContext) {
198
const settings = context.settings || {};
199
203
- const additionalEffectHooks = getAdditionalEffectHooksFromSettings(settings);
200
+ const additionalEffectHooks =
201
+ getAdditionalEffectHooksFromSettings(settings);
202
203
let lastEffect: CallExpression | null = null;
204
const codePathReactHooksMapStack: Array<