[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
This commit is contained in:
Jordan Brown
2025-11-04 14:59:29 -05:00
committed by GitHub
parent d000261eef
commit 8f8b336734
2 changed files with 356 additions and 0 deletions

View File

@@ -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 <Child onClick={onClick}></Child>;
}
`,
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 <Child onClick={useEffectEvent(() => {
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 <Child onClick={onClick} />
}
// The useEffectEvent function shares an identifier name with the above
component MyOtherComponent(theme: any) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={() => 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 <Child />
}
`,
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 <Bar onClick={foo} />
}
`,
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 <Child onClick={onClick} />
}
`,
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 */}
<Child onClick={onClick}></Child>
<Child onClick={onClick2}></Child>
<Child onClick={onClick3}></Child>
</>;
}
`,
// 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.`,
],
},
],
};

View File

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