@samitouri / QOS-React-2 / commits / 8f8b336734

[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;