[eslint] Fix useEffectEvent checks in component syntax (#35041)
We were not recording uEE calls in component/hook syntax. Easy fix. Added tests matching function component syntax for component syntax + added one for hooks
Jordan Brown committed
Nov 4, 2025 at 14:59 UTC
8f8b336734d7c807f5aa11b0f31540e63302d789
2 files changed
+356
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+344
@@ -585,6 +585,29 @@ const allTests = {
585
code: normalizeIndent`
586
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
587
function MyComponent({ theme }) {
588
+ const onClick = useEffectEvent(() => {
589
+ showNotification(theme);
590
+ });
591
+ useMyEffect(() => {
592
+ onClick();
593
+ });
594
+ useServerEffect(() => {
595
+ onClick();
596
+ });
597
+ }
598
+ `,
599
+ settings: {
600
+ 'react-hooks': {
601
+ additionalEffectHooks: '(useMyEffect|useServerEffect)',
602
+ },
603
+ },
604
+ },
605
+ {
606
+ syntax: 'flow',
607
+ code: normalizeIndent`
608
+ // Component syntax version
609
+ // Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
610
+ component MyComponent(theme: any) {
611
const onClick = useEffectEvent(() => {
612
showNotification(theme);
613
});
@@ -618,6 +641,24 @@ const allTests = {
641
}
642
`,
643
},
644
+ {
645
+ syntax: 'flow',
646
+ code: normalizeIndent`
647
+ // Component syntax version
648
+ // Valid because functions created with useEffectEvent can be called in a useEffect.
649
+ component MyComponent(theme: any) {
650
+ const onClick = useEffectEvent(() => {
651
+ showNotification(theme);
652
+ });
653
+ useEffect(() => {
654
+ onClick();
655
+ });
656
+ React.useEffect(() => {
657
+ onClick();
658
+ });
659
+ }
660
+ `,
661
+ },
662
{
663
code: normalizeIndent`
664
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
@@ -644,6 +685,34 @@ const allTests = {
685
}
686
`,
687
},
688
+ {
689
+ syntax: 'flow',
690
+ code: normalizeIndent`
691
+ // Component syntax version
692
+ // Valid because functions created with useEffectEvent can be passed by reference in useEffect
693
+ // and useEffectEvent.
694
+ component MyComponent(theme: any) {
695
+ const onClick = useEffectEvent(() => {
696
+ showNotification(theme);
697
+ });
698
+ const onClick2 = useEffectEvent(() => {
699
+ debounce(onClick);
700
+ debounce(() => onClick());
701
+ debounce(() => { onClick() });
702
+ deboucne(() => debounce(onClick));
703
+ });
704
+ useEffect(() => {
705
+ let id = setInterval(() => onClick(), 100);
706
+ return () => clearInterval(onClick);
707
+ }, []);
708
+ React.useEffect(() => {
709
+ let id = setInterval(() => onClick(), 100);
710
+ return () => clearInterval(onClick);
711
+ }, []);
712
+ return null;
713
+ }
714
+ `,
715
+ },
716
{
717
code: normalizeIndent`
718
function MyComponent({ theme }) {
@@ -656,6 +725,20 @@ const allTests = {
725
}
726
`,
727
},
728
+ {
729
+ syntax: 'flow',
730
+ code: normalizeIndent`
731
+ // Component syntax version
732
+ component MyComponent(theme: any) {
733
+ useEffect(() => {
734
+ onClick();
735
+ });
736
+ const onClick = useEffectEvent(() => {
737
+ showNotification(theme);
738
+ });
739
+ }
740
+ `,
741
+ },
742
{
743
code: normalizeIndent`
744
function MyComponent({ theme }) {
@@ -673,6 +756,25 @@ const allTests = {
756
}
757
`,
758
},
759
+ {
760
+ syntax: 'flow',
761
+ code: normalizeIndent`
762
+ // Component syntax version
763
+ component MyComponent(theme: any) {
764
+ // Can receive arguments
765
+ const onEvent = useEffectEvent((text) => {
766
+ console.log(text);
767
+ });
768
+
769
+ useEffect(() => {
770
+ onEvent('Hello world');
771
+ });
772
+ React.useEffect(() => {
773
+ onEvent('Hello world');
774
+ });
775
+ }
776
+ `,
777
+ },
778
{
779
code: normalizeIndent`
780
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
@@ -689,6 +791,24 @@ const allTests = {
791
}
792
`,
793
},
794
+ {
795
+ syntax: 'flow',
796
+ code: normalizeIndent`
797
+ // Component syntax version
798
+ // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
799
+ component MyComponent(theme: any) {
800
+ const onClick = useEffectEvent(() => {
801
+ showNotification(theme);
802
+ });
803
+ useLayoutEffect(() => {
804
+ onClick();
805
+ });
806
+ React.useLayoutEffect(() => {
807
+ onClick();
808
+ });
809
+ }
810
+ `,
811
+ },
812
{
813
code: normalizeIndent`
814
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
@@ -705,6 +825,24 @@ const allTests = {
825
}
826
`,
827
},
828
+ {
829
+ syntax: 'flow',
830
+ code: normalizeIndent`
831
+ // Component syntax version
832
+ // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
833
+ component MyComponent(theme) {
834
+ const onClick = useEffectEvent(() => {
835
+ showNotification(theme);
836
+ });
837
+ useInsertionEffect(() => {
838
+ onClick();
839
+ });
840
+ React.useInsertionEffect(() => {
841
+ onClick();
842
+ });
843
+ }
844
+ `,
845
+ },
846
{
847
code: normalizeIndent`
848
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
@@ -739,6 +877,42 @@ const allTests = {
877
}
878
`,
879
},
880
+ {
881
+ syntax: 'flow',
882
+ code: normalizeIndent`
883
+ // Component syntax version
884
+ // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect.
885
+ // and useInsertionEffect.
886
+ component MyComponent(theme: any) {
887
+ const onClick = useEffectEvent(() => {
888
+ showNotification(theme);
889
+ });
890
+ const onClick2 = useEffectEvent(() => {
891
+ debounce(onClick);
892
+ debounce(() => onClick());
893
+ debounce(() => { onClick() });
894
+ deboucne(() => debounce(onClick));
895
+ });
896
+ useLayoutEffect(() => {
897
+ let id = setInterval(() => onClick(), 100);
898
+ return () => clearInterval(onClick);
899
+ }, []);
900
+ React.useLayoutEffect(() => {
901
+ let id = setInterval(() => onClick(), 100);
902
+ return () => clearInterval(onClick);
903
+ }, []);
904
+ useInsertionEffect(() => {
905
+ let id = setInterval(() => onClick(), 100);
906
+ return () => clearInterval(onClick);
907
+ }, []);
908
+ React.useInsertionEffect(() => {
909
+ let id = setInterval(() => onClick(), 100);
910
+ return () => clearInterval(onClick);
911
+ }, []);
912
+ return null;
913
+ }
914
+ `,
915
+ },
916
],
917
invalid: [
918
{
@@ -1525,6 +1699,22 @@ const allTests = {
1699
`,
1700
errors: [useEffectEventError('onClick', true)],
1701
},
1702
+ {
1703
+ syntax: 'flow',
1704
+ code: normalizeIndent`
1705
+ // Component syntax version
1706
+ // Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
1707
+ component MyComponent() {
1708
+ const onClick = useEffectEvent(() => {
1709
+ showNotification(theme);
1710
+ });
1711
+ useCustomHook(() => {
1712
+ onClick();
1713
+ });
1714
+ }
1715
+ `,
1716
+ errors: [useEffectEventError('onClick', true)],
1717
+ },
1718
{
1719
code: normalizeIndent`
1720
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
@@ -1544,6 +1734,27 @@ const allTests = {
1734
},
1735
errors: [useEffectEventError('onClick', true)],
1736
},
1737
+ {
1738
+ syntax: 'flow',
1739
+ code: normalizeIndent`
1740
+ // Component syntax version
1741
+ // Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
1742
+ component MyComponent(theme: any) {
1743
+ const onClick = useEffectEvent(() => {
1744
+ showNotification(theme);
1745
+ });
1746
+ useWrongHook(() => {
1747
+ onClick();
1748
+ });
1749
+ }
1750
+ `,
1751
+ settings: {
1752
+ 'react-hooks': {
1753
+ additionalEffectHooks: 'useMyEffect',
1754
+ },
1755
+ },
1756
+ errors: [useEffectEventError('onClick', true)],
1757
+ },
1758
{
1759
code: normalizeIndent`
1760
function MyComponent({ theme }) {
@@ -1555,6 +1766,19 @@ const allTests = {
1766
`,
1767
errors: [useEffectEventError('onClick', false)],
1768
},
1769
+ {
1770
+ syntax: 'flow',
1771
+ code: normalizeIndent`
1772
+ // Component syntax version
1773
+ component MyComponent(theme: any) {
1774
+ const onClick = useEffectEvent(() => {
1775
+ showNotification(theme);
1776
+ });
1777
+ return <Child onClick={onClick}></Child>;
1778
+ }
1779
+ `,
1780
+ errors: [useEffectEventError('onClick', false)],
1781
+ },
1782
{
1783
code: normalizeIndent`
1784
// Invalid because useEffectEvent is being passed down
@@ -1566,6 +1790,19 @@ const allTests = {
1790
`,
1791
errors: [{...useEffectEventError(null, false), line: 4}],
1792
},
1793
+ {
1794
+ syntax: 'flow',
1795
+ code: normalizeIndent`
1796
+ // Component syntax version
1797
+ // Invalid because useEffectEvent is being passed down
1798
+ component MyComponent(theme: any) {
1799
+ return <Child onClick={useEffectEvent(() => {
1800
+ showNotification(theme);
1801
+ })} />;
1802
+ }
1803
+ `,
1804
+ errors: [{...useEffectEventError(null, false), line: 5}],
1805
+ },
1806
{
1807
code: normalizeIndent`
1808
// This should error even though it shares an identifier name with the below
@@ -1601,6 +1838,43 @@ const allTests = {
1838
{...useEffectEventError('onClick', true), line: 15},
1839
],
1840
},
1841
+ {
1842
+ syntax: 'flow',
1843
+ code: normalizeIndent`
1844
+ // Component syntax version
1845
+ // This should error even though it shares an identifier name with the below
1846
+ component MyComponent(theme: any) {
1847
+ const onClick = useEffectEvent(() => {
1848
+ showNotification(theme)
1849
+ });
1850
+ return <Child onClick={onClick} />
1851
+ }
1852
+
1853
+ // The useEffectEvent function shares an identifier name with the above
1854
+ component MyOtherComponent(theme: any) {
1855
+ const onClick = useEffectEvent(() => {
1856
+ showNotification(theme)
1857
+ });
1858
+ return <Child onClick={() => onClick()} />
1859
+ }
1860
+
1861
+ // The useEffectEvent function shares an identifier name with the above
1862
+ component MyLastComponent(theme: any) {
1863
+ const onClick = useEffectEvent(() => {
1864
+ showNotification(theme)
1865
+ });
1866
+ useEffect(() => {
1867
+ onClick(); // No error here, errors on all other uses
1868
+ onClick;
1869
+ })
1870
+ return <Child />
1871
+ }
1872
+ `,
1873
+ errors: [
1874
+ {...useEffectEventError('onClick', false), line: 8},
1875
+ {...useEffectEventError('onClick', true), line: 16},
1876
+ ],
1877
+ },
1878
{
1879
code: normalizeIndent`
1880
const MyComponent = ({ theme }) => {
@@ -1625,6 +1899,21 @@ const allTests = {
1899
`,
1900
errors: [{...useEffectEventError('onClick', false), line: 7}],
1901
},
1902
+ {
1903
+ syntax: 'flow',
1904
+ code: normalizeIndent`
1905
+ // Component syntax version
1906
+ // Invalid because onClick is being aliased to foo but not invoked
1907
+ component MyComponent(theme: any) {
1908
+ const onClick = useEffectEvent(() => {
1909
+ showNotification(theme);
1910
+ });
1911
+ let foo = onClick;
1912
+ return <Bar onClick={foo} />
1913
+ }
1914
+ `,
1915
+ errors: [{...useEffectEventError('onClick', false), line: 8}],
1916
+ },
1917
{
1918
code: normalizeIndent`
1919
// Should error because it's being passed down to JSX, although it's been referenced once
@@ -1641,6 +1930,24 @@ const allTests = {
1930
`,
1931
errors: [useEffectEventError('onClick', false)],
1932
},
1933
+ {
1934
+ syntax: 'flow',
1935
+ code: normalizeIndent`
1936
+ // Component syntax version
1937
+ // Should error because it's being passed down to JSX, although it's been referenced once
1938
+ // in an effect
1939
+ component MyComponent(theme: any) {
1940
+ const onClick = useEffectEvent(() => {
1941
+ showNotification(them);
1942
+ });
1943
+ useEffect(() => {
1944
+ setTimeout(onClick, 100);
1945
+ });
1946
+ return <Child onClick={onClick} />
1947
+ }
1948
+ `,
1949
+ errors: [useEffectEventError('onClick', false)],
1950
+ },
1951
{
1952
code: normalizeIndent`
1953
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
@@ -1676,6 +1983,43 @@ const allTests = {
1983
`It cannot be assigned to a variable or passed down.`,
1984
],
1985
},
1986
+ {
1987
+ syntax: 'flow',
1988
+ code: normalizeIndent`
1989
+ // Hook syntax version
1990
+ // Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
1991
+ hook useMyHook(theme: any) {
1992
+ const onClick = useEffectEvent(() => {
1993
+ showNotification(theme);
1994
+ });
1995
+ // error message 1
1996
+ const onClick2 = () => { onClick() };
1997
+ // error message 2
1998
+ const onClick3 = useCallback(() => onClick(), []);
1999
+ // error message 3
2000
+ const onClick4 = onClick;
2001
+ return <>
2002
+ {/** error message 4 */}
2003
+ <Child onClick={onClick}></Child>
2004
+ <Child onClick={onClick2}></Child>
2005
+ <Child onClick={onClick3}></Child>
2006
+ </>;
2007
+ }
2008
+ `,
2009
+ // Explicitly test error messages here for various cases
2010
+ errors: [
2011
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2012
+ 'Effects and Effect Events in the same component.',
2013
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2014
+ 'Effects and Effect Events in the same component.',
2015
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2016
+ `Effects and Effect Events in the same component. ` +
2017
+ `It cannot be assigned to a variable or passed down.`,
2018
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2019
+ `Effects and Effect Events in the same component. ` +
2020
+ `It cannot be assigned to a variable or passed down.`,
2021
+ ],
2022
+ },
2023
],
2024
};
2025
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+12
@@ -833,6 +833,18 @@ const rule = {
833
recordAllUseEffectEventFunctions(getScope(node));
834
}
835
},
836
+
837
+ // @ts-expect-error parser-hermes produces these node types
838
+ ComponentDeclaration(node) {
839
+ // component MyComponent() { const onClick = useEffectEvent(...) }
840
+ recordAllUseEffectEventFunctions(getScope(node));
841
+ },
842
+
843
+ // @ts-expect-error parser-hermes produces these node types
844
+ HookDeclaration(node) {
845
+ // hook useMyHook() { const onClick = useEffectEvent(...) }
846
+ recordAllUseEffectEventFunctions(getScope(node));
847
+ },
848
};
849
},
850
} satisfies Rule.RuleModule;