Compare commits

...

21 Commits

Author SHA1 Message Date
Jordan Brown
30e04b6af5 merge commit for archive created by Sapling 2025-11-04 12:59:49 -05:00
Jordan Brown
b8b2373a05 [eslint] Fix useEffectEvent checks in component syntax 2025-11-04 12:56:55 -05:00
Jordan Brown
aff4530071 Merge e3b8eadab0 into sapling-pr-archive-jbrown215 2025-11-04 12:45:14 -05:00
Jordan Brown
e3b8eadab0 [eslint] Fix useEffectEvent checks in component syntax 2025-11-04 12:45:07 -05:00
Jordan Brown
df079f070d merge commit for archive created by Sapling 2025-10-01 12:59:22 -04:00
Jordan Brown
820700dfc3 [flow] Add web host element libdefs 2025-10-01 12:59:07 -04:00
Jordan Brown
e11adc8689 merge commit for archive created by Sapling 2025-09-30 07:20:40 -04:00
Jordan Brown
0357e0abed [lint] Use settings for additional hooks in exhaustive deps
Like in the diff below, we can read from the shared configuration to check exhaustive deps.

I allow the classic additionalHooks configuration to override it so that this change
is backwards compatible.


--
2025-09-30 07:20:29 -04:00
Jordan Brown
ac4ac1f5a6 [lint] Enable custom hooks configuration for useEffectEvent calling rules
We need to be able to specify additional effect hooks for the RulesOfHooks lint rule
in order to allow useEffectEvent to be called by custom effects. ExhaustiveDeps
does this with a regex suppplied to the rule, but that regex is not accessible from
other rules.

This diff introduces a `react-eslint` entry you can put in the eslint settings that
allows you to specify custom effect hooks and share them across all rules.

This works like:
```
{
  settings: {
    'react-eslint': {
      additionalEffectHooks: string,
    },
  },
}
```

The next diff allows useEffect to read from the same configuration.


----
2025-09-30 07:13:44 -04:00
Jordan Brown
34daed9504 Merge 5298c84d9e into sapling-pr-archive-jbrown215 2025-09-28 14:36:44 -04:00
Jordan Brown
5298c84d9e [lint] Use settings for additional hooks in exhaustive deps
Like in the diff below, we can read from the shared configuration to check exhaustive deps.

I allow the classic additionalHooks configuration to override it so that this change
is backwards compatible.


--
2025-09-28 14:36:34 -04:00
Jordan Brown
4e248b3047 [lint] Enable custom hooks configuration for useEffectEvent calling rules
We need to be able to specify additional effect hooks for the RulesOfHooks lint rule
in order to allow useEffectEvent to be called by custom effects. ExhaustiveDeps
does this with a regex suppplied to the rule, but that regex is not accessible from
other rules.

This diff introduces a `react-eslint` entry you can put in the eslint settings that
allows you to specify custom effect hooks and share them across all rules.

This works like:
```
{
  settings: {
    'react-eslint': {
      additionalEffectHooks: string,
    },
  },
}
```

The next diff allows useEffect to read from the same configuration.


----
2025-09-28 14:35:36 -04:00
Jordan Brown
cf1dfa86f3 Merge f0e5627b3e into sapling-pr-archive-jbrown215 2025-09-28 14:32:36 -04:00
Jordan Brown
f0e5627b3e [lint] Use settings for additional hooks in exhaustive deps 2025-09-28 14:32:27 -04:00
Jordan Brown
03646a6ed2 merge commit for archive created by Sapling 2025-09-28 13:14:02 -04:00
Jordan Brown
d08f2e4bd9 [lint] Enable custom hooks configuration for useEffectEvent calling rules 2025-09-28 13:13:53 -04:00
Jordan Brown
6679f4ae32 merge commit for archive created by Sapling 2025-09-28 13:06:56 -04:00
Jordan Brown
608bc3923f [lint] Enable custom hooks configuration for useEffectEvent calling rules 2025-09-28 13:06:49 -04:00
Jordan Brown
e70cbe0fff Merge 984ccbac71 into sapling-pr-archive-jbrown215 2025-09-15 12:39:54 -04:00
Jordan Brown
984ccbac71 [lint] Enable custom hooks configuration for useEffectEvent calling rules 2025-09-15 12:39:33 -04:00
Jordan Brown
28a86ffe47 [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect 2025-09-15 09:11:08 -04:00
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;