diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
index 05bdb1e71ed..3d60a36824d 100644
--- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
@@ -585,6 +585,29 @@ const allTests = {
code: normalizeIndent`
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
function MyComponent({ theme }) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useMyEffect(() => {
+ onClick();
+ });
+ useServerEffect(() => {
+ onClick();
+ });
+ }
+ `,
+ settings: {
+ 'react-hooks': {
+ additionalEffectHooks: '(useMyEffect|useServerEffect)',
+ },
+ },
+ },
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
+ component MyComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
@@ -618,6 +641,24 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid because functions created with useEffectEvent can be called in a useEffect.
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useEffect(() => {
+ onClick();
+ });
+ React.useEffect(() => {
+ onClick();
+ });
+ }
+ `,
+ },
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
@@ -644,6 +685,34 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid because functions created with useEffectEvent can be passed by reference in useEffect
+ // and useEffectEvent.
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ const onClick2 = useEffectEvent(() => {
+ debounce(onClick);
+ debounce(() => onClick());
+ debounce(() => { onClick() });
+ deboucne(() => debounce(onClick));
+ });
+ useEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ React.useEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ return null;
+ }
+ `,
+ },
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -656,6 +725,20 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ component MyComponent(theme: any) {
+ useEffect(() => {
+ onClick();
+ });
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ }
+ `,
+ },
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -673,6 +756,25 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ component MyComponent(theme: any) {
+ // Can receive arguments
+ const onEvent = useEffectEvent((text) => {
+ console.log(text);
+ });
+
+ useEffect(() => {
+ onEvent('Hello world');
+ });
+ React.useEffect(() => {
+ onEvent('Hello world');
+ });
+ }
+ `,
+ },
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
@@ -689,6 +791,24 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useLayoutEffect(() => {
+ onClick();
+ });
+ React.useLayoutEffect(() => {
+ onClick();
+ });
+ }
+ `,
+ },
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
@@ -705,6 +825,24 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
+ component MyComponent(theme) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useInsertionEffect(() => {
+ onClick();
+ });
+ React.useInsertionEffect(() => {
+ onClick();
+ });
+ }
+ `,
+ },
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
@@ -739,6 +877,42 @@ const allTests = {
}
`,
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect.
+ // and useInsertionEffect.
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ const onClick2 = useEffectEvent(() => {
+ debounce(onClick);
+ debounce(() => onClick());
+ debounce(() => { onClick() });
+ deboucne(() => debounce(onClick));
+ });
+ useLayoutEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ React.useLayoutEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ useInsertionEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ React.useInsertionEffect(() => {
+ let id = setInterval(() => onClick(), 100);
+ return () => clearInterval(onClick);
+ }, []);
+ return null;
+ }
+ `,
+ },
],
invalid: [
{
@@ -1525,6 +1699,22 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', true)],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
+ component MyComponent() {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useCustomHook(() => {
+ onClick();
+ });
+ }
+ `,
+ errors: [useEffectEventError('onClick', true)],
+ },
{
code: normalizeIndent`
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
@@ -1544,6 +1734,27 @@ const allTests = {
},
errors: [useEffectEventError('onClick', true)],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ useWrongHook(() => {
+ onClick();
+ });
+ }
+ `,
+ settings: {
+ 'react-hooks': {
+ additionalEffectHooks: 'useMyEffect',
+ },
+ },
+ errors: [useEffectEventError('onClick', true)],
+ },
{
code: normalizeIndent`
function MyComponent({ theme }) {
@@ -1555,6 +1766,19 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ return ;
+ }
+ `,
+ errors: [useEffectEventError('onClick', false)],
+ },
{
code: normalizeIndent`
// Invalid because useEffectEvent is being passed down
@@ -1566,6 +1790,19 @@ const allTests = {
`,
errors: [{...useEffectEventError(null, false), line: 4}],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Invalid because useEffectEvent is being passed down
+ component MyComponent(theme: any) {
+ return {
+ showNotification(theme);
+ })} />;
+ }
+ `,
+ errors: [{...useEffectEventError(null, false), line: 5}],
+ },
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
@@ -1601,6 +1838,43 @@ const allTests = {
{...useEffectEventError('onClick', true), line: 15},
],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // This should error even though it shares an identifier name with the below
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme)
+ });
+ return
+ }
+
+ // The useEffectEvent function shares an identifier name with the above
+ component MyOtherComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme)
+ });
+ return onClick()} />
+ }
+
+ // The useEffectEvent function shares an identifier name with the above
+ component MyLastComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme)
+ });
+ useEffect(() => {
+ onClick(); // No error here, errors on all other uses
+ onClick;
+ })
+ return
+ }
+ `,
+ errors: [
+ {...useEffectEventError('onClick', false), line: 8},
+ {...useEffectEventError('onClick', true), line: 16},
+ ],
+ },
{
code: normalizeIndent`
const MyComponent = ({ theme }) => {
@@ -1625,6 +1899,21 @@ const allTests = {
`,
errors: [{...useEffectEventError('onClick', false), line: 7}],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Invalid because onClick is being aliased to foo but not invoked
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ let foo = onClick;
+ return
+ }
+ `,
+ errors: [{...useEffectEventError('onClick', false), line: 8}],
+ },
{
code: normalizeIndent`
// Should error because it's being passed down to JSX, although it's been referenced once
@@ -1641,6 +1930,24 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Component syntax version
+ // Should error because it's being passed down to JSX, although it's been referenced once
+ // in an effect
+ component MyComponent(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(them);
+ });
+ useEffect(() => {
+ setTimeout(onClick, 100);
+ });
+ return
+ }
+ `,
+ errors: [useEffectEventError('onClick', false)],
+ },
{
code: normalizeIndent`
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
@@ -1676,6 +1983,43 @@ const allTests = {
`It cannot be assigned to a variable or passed down.`,
],
},
+ {
+ syntax: 'flow',
+ code: normalizeIndent`
+ // Hook syntax version
+ // Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
+ hook useMyHook(theme: any) {
+ const onClick = useEffectEvent(() => {
+ showNotification(theme);
+ });
+ // error message 1
+ const onClick2 = () => { onClick() };
+ // error message 2
+ const onClick3 = useCallback(() => onClick(), []);
+ // error message 3
+ const onClick4 = onClick;
+ return <>
+ {/** error message 4 */}
+
+
+
+ >;
+ }
+ `,
+ // Explicitly test error messages here for various cases
+ errors: [
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
+ 'Effects and Effect Events in the same component.',
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
+ 'Effects and Effect Events in the same component.',
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
+ `Effects and Effect Events in the same component. ` +
+ `It cannot be assigned to a variable or passed down.`,
+ `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
+ `Effects and Effect Events in the same component. ` +
+ `It cannot be assigned to a variable or passed down.`,
+ ],
+ },
],
};
diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
index 4e49e96bfe6..ca82c99e2f5 100644
--- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
@@ -833,6 +833,18 @@ const rule = {
recordAllUseEffectEventFunctions(getScope(node));
}
},
+
+ // @ts-expect-error parser-hermes produces these node types
+ ComponentDeclaration(node) {
+ // component MyComponent() { const onClick = useEffectEvent(...) }
+ recordAllUseEffectEventFunctions(getScope(node));
+ },
+
+ // @ts-expect-error parser-hermes produces these node types
+ HookDeclaration(node) {
+ // hook useMyHook() { const onClick = useEffectEvent(...) }
+ recordAllUseEffectEventFunctions(getScope(node));
+ },
};
},
} satisfies Rule.RuleModule;