Skip to content

Commit 4e248b3

Browse files
committed
[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. ----
1 parent 09d3cd8 commit 4e248b3

File tree

2 files changed

+90
-3
lines changed

2 files changed

+90
-3
lines changed

packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,27 @@ const allTests = {
581581
};
582582
`,
583583
},
584+
{
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-eslint': {
601+
additionalEffectHooks: '(useMyEffect|useServerEffect)',
602+
},
603+
},
604+
},
584605
],
585606
invalid: [
586607
{
@@ -1353,6 +1374,39 @@ const allTests = {
13531374
`,
13541375
errors: [tryCatchUseError('use')],
13551376
},
1377+
{
1378+
code: normalizeIndent`
1379+
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
1380+
function MyComponent({ theme }) {
1381+
const onClick = useEffectEvent(() => {
1382+
showNotification(theme);
1383+
});
1384+
useCustomHook(() => {
1385+
onClick();
1386+
});
1387+
}
1388+
`,
1389+
errors: [useEffectEventError('onClick', true)],
1390+
},
1391+
{
1392+
code: normalizeIndent`
1393+
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
1394+
function MyComponent({ theme }) {
1395+
const onClick = useEffectEvent(() => {
1396+
showNotification(theme);
1397+
});
1398+
useWrongHook(() => {
1399+
onClick();
1400+
});
1401+
}
1402+
`,
1403+
settings: {
1404+
'react-eslint': {
1405+
additionalEffectHooks: 'useMyEffect',
1406+
},
1407+
},
1408+
errors: [useEffectEventError('onClick', true)],
1409+
},
13561410
],
13571411
};
13581412

packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,23 @@ function getNodeWithoutReactNamespace(
147147
return node;
148148
}
149149

150-
function isEffectIdentifier(node: Node): boolean {
151-
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
150+
function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {
151+
const isBuiltInEffect =
152+
node.type === 'Identifier' &&
153+
(node.name === 'useEffect' ||
154+
node.name === 'useLayoutEffect' ||
155+
node.name === 'useInsertionEffect');
156+
157+
if (isBuiltInEffect) {
158+
return true;
159+
}
160+
161+
// Check if this matches additional hooks configured by the user
162+
if (additionalHooks && node.type === 'Identifier') {
163+
return additionalHooks.test(node.name);
164+
}
165+
166+
return false;
152167
}
153168
function isUseEffectEventIdentifier(node: Node): boolean {
154169
if (__EXPERIMENTAL__) {
@@ -169,8 +184,26 @@ const rule = {
169184
recommended: true,
170185
url: 'https://react.dev/reference/rules/rules-of-hooks',
171186
},
187+
schema: [
188+
{
189+
type: 'object',
190+
additionalProperties: false,
191+
properties: {
192+
additionalHooks: {
193+
type: 'string',
194+
},
195+
},
196+
},
197+
],
172198
},
173199
create(context: Rule.RuleContext) {
200+
const settings = context.settings || {};
201+
202+
const additionalEffectHooks =
203+
settings['react-eslint'] && settings['react-eslint'].additionalEffectHooks
204+
? new RegExp(settings['react-eslint'].additionalEffectHooks)
205+
: undefined;
206+
174207
let lastEffect: CallExpression | null = null;
175208
const codePathReactHooksMapStack: Array<
176209
Map<Rule.CodePathSegment, Array<Node>>
@@ -726,7 +759,7 @@ const rule = {
726759
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
727760
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
728761
if (
729-
(isEffectIdentifier(nodeWithoutNamespace) ||
762+
(isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) ||
730763
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
731764
node.arguments.length > 0
732765
) {

0 commit comments

Comments
 (0)