[lint] Enable custom hooks configuration for useEffectEvent calling rules

This commit is contained in:
Jordan Brown
2025-09-28 12:59:20 -04:00
parent 09d3cd8fb5
commit d08f2e4bd9
2 changed files with 90 additions and 3 deletions
@@ -581,6 +581,27 @@ 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-eslint': {
additionalEffectHooks: '(useMyEffect|useServerEffect)',
},
},
},
],
invalid: [
{
@@ -1353,6 +1374,39 @@ const allTests = {
`,
errors: [tryCatchUseError('use')],
},
{
code: normalizeIndent`
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
function MyComponent({ theme }) {
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
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useWrongHook(() => {
onClick();
});
}
`,
settings: {
'react-eslint': {
additionalEffectHooks: 'useMyEffect',
},
},
errors: [useEffectEventError('onClick', true)],
},
],
};
@@ -147,8 +147,23 @@ function getNodeWithoutReactNamespace(
return node;
}
function isEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {
const isBuiltInEffect =
node.type === 'Identifier' &&
(node.name === 'useEffect' ||
node.name === 'useLayoutEffect' ||
node.name === 'useInsertionEffect');
if (isBuiltInEffect) {
return true;
}
// Check if this matches additional hooks configured by the user
if (additionalHooks && node.type === 'Identifier') {
return additionalHooks.test(node.name);
}
return false;
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
@@ -169,8 +184,26 @@ const rule = {
recommended: true,
url: 'https://react.dev/reference/rules/rules-of-hooks',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
additionalHooks: {
type: 'string',
},
},
},
],
},
create(context: Rule.RuleContext) {
const settings = context.settings || {};
const additionalEffectHooks =
settings['react-eslint'] && settings['react-eslint'].additionalEffectHooks
? new RegExp(settings['react-eslint'].additionalEffectHooks)
: undefined;
let lastEffect: CallExpression | null = null;
const codePathReactHooksMapStack: Array<
Map<Rule.CodePathSegment, Array<Node>>
@@ -726,7 +759,7 @@ const rule = {
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
if (
(isEffectIdentifier(nodeWithoutNamespace) ||
(isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) ||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
node.arguments.length > 0
) {