Skip to content

Commit a3abf5f

Browse files
authored
[eslint-plugin-react-hooks] add experimental_autoDependenciesHooks option (#33294)
1 parent 462d08f commit a3abf5f

File tree

2 files changed

+157
-10
lines changed

2 files changed

+157
-10
lines changed

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,22 @@ const tests = {
515515
`,
516516
options: [{additionalHooks: 'useCustomEffect'}],
517517
},
518+
{
519+
// behaves like no deps
520+
code: normalizeIndent`
521+
function MyComponent(props) {
522+
useSpecialEffect(() => {
523+
console.log(props.foo);
524+
}, null);
525+
}
526+
`,
527+
options: [
528+
{
529+
additionalHooks: 'useSpecialEffect',
530+
experimental_autoDependenciesHooks: ['useSpecialEffect'],
531+
},
532+
],
533+
},
518534
{
519535
code: normalizeIndent`
520536
function MyComponent(props) {
@@ -1470,6 +1486,38 @@ const tests = {
14701486
},
14711487
],
14721488
invalid: [
1489+
{
1490+
code: normalizeIndent`
1491+
function MyComponent(props) {
1492+
useSpecialEffect(() => {
1493+
console.log(props.foo);
1494+
}, null);
1495+
}
1496+
`,
1497+
options: [{additionalHooks: 'useSpecialEffect'}],
1498+
errors: [
1499+
{
1500+
message:
1501+
"React Hook useSpecialEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.",
1502+
},
1503+
{
1504+
message:
1505+
"React Hook useSpecialEffect has a missing dependency: 'props.foo'. Either include it or remove the dependency array.",
1506+
suggestions: [
1507+
{
1508+
desc: 'Update the dependencies array to be: [props.foo]',
1509+
output: normalizeIndent`
1510+
function MyComponent(props) {
1511+
useSpecialEffect(() => {
1512+
console.log(props.foo);
1513+
}, [props.foo]);
1514+
}
1515+
`,
1516+
},
1517+
],
1518+
},
1519+
],
1520+
},
14731521
{
14741522
code: normalizeIndent`
14751523
function MyComponent(props) {
@@ -7821,6 +7869,24 @@ const testsTypescript = {
78217869
}
78227870
`,
78237871
},
7872+
{
7873+
code: normalizeIndent`
7874+
function MyComponent() {
7875+
const [state, setState] = React.useState<number>(0);
7876+
7877+
useSpecialEffect(() => {
7878+
const someNumber: typeof state = 2;
7879+
setState(prevState => prevState + someNumber);
7880+
})
7881+
}
7882+
`,
7883+
options: [
7884+
{
7885+
additionalHooks: 'useSpecialEffect',
7886+
experimental_autoDependenciesHooks: ['useSpecialEffect'],
7887+
},
7888+
],
7889+
},
78247890
{
78257891
code: normalizeIndent`
78267892
function App() {
@@ -8176,6 +8242,48 @@ const testsTypescript = {
81768242
function MyComponent() {
81778243
const [state, setState] = React.useState<number>(0);
81788244
8245+
useSpecialEffect(() => {
8246+
const someNumber: typeof state = 2;
8247+
setState(prevState => prevState + someNumber + state);
8248+
}, [])
8249+
}
8250+
`,
8251+
options: [
8252+
{
8253+
additionalHooks: 'useSpecialEffect',
8254+
experimental_autoDependenciesHooks: ['useSpecialEffect'],
8255+
},
8256+
],
8257+
errors: [
8258+
{
8259+
message:
8260+
"React Hook useSpecialEffect has a missing dependency: 'state'. " +
8261+
'Either include it or remove the dependency array. ' +
8262+
`You can also do a functional update 'setState(s => ...)' ` +
8263+
`if you only need 'state' in the 'setState' call.`,
8264+
suggestions: [
8265+
{
8266+
desc: 'Update the dependencies array to be: [state]',
8267+
output: normalizeIndent`
8268+
function MyComponent() {
8269+
const [state, setState] = React.useState<number>(0);
8270+
8271+
useSpecialEffect(() => {
8272+
const someNumber: typeof state = 2;
8273+
setState(prevState => prevState + someNumber + state);
8274+
}, [state])
8275+
}
8276+
`,
8277+
},
8278+
],
8279+
},
8280+
],
8281+
},
8282+
{
8283+
code: normalizeIndent`
8284+
function MyComponent() {
8285+
const [state, setState] = React.useState<number>(0);
8286+
81798287
useMemo(() => {
81808288
const someNumber: typeof state = 2;
81818289
console.log(someNumber);

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

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,38 @@ const rule = {
6161
enableDangerousAutofixThisMayCauseInfiniteLoops: {
6262
type: 'boolean',
6363
},
64+
experimental_autoDependenciesHooks: {
65+
type: 'array',
66+
items: {
67+
type: 'string',
68+
},
69+
},
6470
},
6571
},
6672
],
6773
},
6874
create(context: Rule.RuleContext) {
75+
const rawOptions = context.options && context.options[0];
76+
6977
// Parse the `additionalHooks` regex.
7078
const additionalHooks =
71-
context.options &&
72-
context.options[0] &&
73-
context.options[0].additionalHooks
74-
? new RegExp(context.options[0].additionalHooks)
79+
rawOptions && rawOptions.additionalHooks
80+
? new RegExp(rawOptions.additionalHooks)
7581
: undefined;
7682

7783
const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean =
78-
(context.options &&
79-
context.options[0] &&
80-
context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
84+
(rawOptions &&
85+
rawOptions.enableDangerousAutofixThisMayCauseInfiniteLoops) ||
8186
false;
8287

88+
const experimental_autoDependenciesHooks: ReadonlyArray<string> =
89+
rawOptions && Array.isArray(rawOptions.experimental_autoDependenciesHooks)
90+
? rawOptions.experimental_autoDependenciesHooks
91+
: [];
92+
8393
const options = {
8494
additionalHooks,
95+
experimental_autoDependenciesHooks,
8596
enableDangerousAutofixThisMayCauseInfiniteLoops,
8697
};
8798

@@ -162,6 +173,7 @@ const rule = {
162173
reactiveHook: Node,
163174
reactiveHookName: string,
164175
isEffect: boolean,
176+
isAutoDepsHook: boolean,
165177
): void {
166178
if (isEffect && node.async) {
167179
reportProblem({
@@ -649,6 +661,9 @@ const rule = {
649661
}
650662

651663
if (!declaredDependenciesNode) {
664+
if (isAutoDepsHook) {
665+
return;
666+
}
652667
// Check if there are any top-level setState() calls.
653668
// Those tend to lead to infinite loops.
654669
let setStateInsideEffectWithoutDeps: string | null = null;
@@ -711,6 +726,13 @@ const rule = {
711726
}
712727
return;
713728
}
729+
if (
730+
isAutoDepsHook &&
731+
declaredDependenciesNode.type === 'Literal' &&
732+
declaredDependenciesNode.value === null
733+
) {
734+
return;
735+
}
714736

715737
const declaredDependencies: Array<DeclaredDependency> = [];
716738
const externalDependencies = new Set<string>();
@@ -1318,10 +1340,19 @@ const rule = {
13181340
return;
13191341
}
13201342

1343+
const isAutoDepsHook =
1344+
options.experimental_autoDependenciesHooks.includes(reactiveHookName);
1345+
13211346
// Check the declared dependencies for this reactive hook. If there is no
13221347
// second argument then the reactive callback will re-run on every render.
13231348
// So no need to check for dependency inclusion.
1324-
if (!declaredDependenciesNode && !isEffect) {
1349+
if (
1350+
(!declaredDependenciesNode ||
1351+
(isAutoDepsHook &&
1352+
declaredDependenciesNode.type === 'Literal' &&
1353+
declaredDependenciesNode.value === null)) &&
1354+
!isEffect
1355+
) {
13251356
// These are only used for optimization.
13261357
if (
13271358
reactiveHookName === 'useMemo' ||
@@ -1355,11 +1386,17 @@ const rule = {
13551386
reactiveHook,
13561387
reactiveHookName,
13571388
isEffect,
1389+
isAutoDepsHook,
13581390
);
13591391
return; // Handled
13601392
case 'Identifier':
1361-
if (!declaredDependenciesNode) {
1362-
// No deps, no problems.
1393+
if (
1394+
!declaredDependenciesNode ||
1395+
(isAutoDepsHook &&
1396+
declaredDependenciesNode.type === 'Literal' &&
1397+
declaredDependenciesNode.value === null)
1398+
) {
1399+
// Always runs, no problems.
13631400
return; // Handled
13641401
}
13651402
// The function passed as a callback is not written inline.
@@ -1408,6 +1445,7 @@ const rule = {
14081445
reactiveHook,
14091446
reactiveHookName,
14101447
isEffect,
1448+
isAutoDepsHook,
14111449
);
14121450
return; // Handled
14131451
case 'VariableDeclarator':
@@ -1427,6 +1465,7 @@ const rule = {
14271465
reactiveHook,
14281466
reactiveHookName,
14291467
isEffect,
1468+
isAutoDepsHook,
14301469
);
14311470
return; // Handled
14321471
}

0 commit comments

Comments
 (0)