Skip to content

Commit 92f00a1

Browse files
committed
[linter] Add configuration option to specify hooks that return stable values
1 parent 14094f8 commit 92f00a1

File tree

2 files changed

+347
-3
lines changed

2 files changed

+347
-3
lines changed

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

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,143 @@ const tests = {
14841484
}
14851485
`,
14861486
},
1487+
{
1488+
code: normalizeIndent`
1489+
function MyComponent() {
1490+
const stableValue = useStableValue();
1491+
useEffect(() => {
1492+
console.log(stableValue);
1493+
}, []); // No need to include stableValue as a dependency
1494+
}
1495+
`,
1496+
options: [
1497+
{
1498+
stableValueHooks: [
1499+
{name: 'useStableValue', propertiesOrIndexes: null},
1500+
],
1501+
},
1502+
],
1503+
},
1504+
{
1505+
code: normalizeIndent`
1506+
function MyComponent() {
1507+
const { stableProp } = usePartiallyStableValue();
1508+
useEffect(() => {
1509+
console.log(stableProp); // stableProp is stable, no need to include as dependency
1510+
}, []);
1511+
}
1512+
`,
1513+
options: [
1514+
{
1515+
stableValueHooks: [
1516+
{
1517+
name: 'usePartiallyStableValue',
1518+
propertiesOrIndexes: ['stableProp'],
1519+
},
1520+
],
1521+
},
1522+
],
1523+
},
1524+
{
1525+
code: normalizeIndent`
1526+
function MyComponent() {
1527+
const [stableItem] = useArrayWithStableItems();
1528+
useEffect(() => {
1529+
console.log(stableItem); // stableItem is stable, no need to include as dependency
1530+
}, []);
1531+
}
1532+
`,
1533+
options: [
1534+
{
1535+
stableValueHooks: [
1536+
{
1537+
name: 'useArrayWithStableItems',
1538+
propertiesOrIndexes: [0],
1539+
},
1540+
],
1541+
},
1542+
],
1543+
},
1544+
{
1545+
code: normalizeIndent`
1546+
function MyComponent() {
1547+
const { prop1, prop2 } = useMultipleStableProps();
1548+
useEffect(() => {
1549+
console.log(prop1, prop2); // prop1 and prop2 are stable
1550+
}, []);
1551+
}
1552+
`,
1553+
options: [
1554+
{
1555+
stableValueHooks: [
1556+
{
1557+
name: 'useMultipleStableProps',
1558+
propertiesOrIndexes: ['prop1', 'prop2'],
1559+
},
1560+
],
1561+
},
1562+
],
1563+
},
1564+
{
1565+
code: normalizeIndent`
1566+
function MyComponent() {
1567+
const [item1, , item3] = useArrayWithMultipleStableItems();
1568+
useEffect(() => {
1569+
console.log(item1, item3); // item1 and item3 are stable
1570+
}, []);
1571+
}
1572+
`,
1573+
options: [
1574+
{
1575+
stableValueHooks: [
1576+
{
1577+
name: 'useArrayWithMultipleStableItems',
1578+
propertiesOrIndexes: [0, 2],
1579+
},
1580+
],
1581+
},
1582+
],
1583+
},
1584+
{
1585+
code: normalizeIndent`
1586+
function MyComponent() {
1587+
const { stableProp, unstableProp } = useMixedStability();
1588+
useEffect(() => {
1589+
console.log(stableProp, unstableProp);
1590+
}, [unstableProp]); // Only unstableProp needs to be included
1591+
}
1592+
`,
1593+
options: [
1594+
{
1595+
stableValueHooks: [
1596+
{
1597+
name: 'useMixedStability',
1598+
propertiesOrIndexes: ['stableProp'],
1599+
},
1600+
],
1601+
},
1602+
],
1603+
},
1604+
{
1605+
code: normalizeIndent`
1606+
function MyComponent() {
1607+
const [stableItem, unstableItem] = useArrayWithStableItems();
1608+
useEffect(() => {
1609+
console.log(stableItem, unstableItem);
1610+
}, [stableItem, unstableItem]); // Including stableItem is allowed
1611+
}
1612+
`,
1613+
options: [
1614+
{
1615+
stableValueHooks: [
1616+
{
1617+
name: 'useArrayWithStableItems',
1618+
propertiesOrIndexes: [0],
1619+
},
1620+
],
1621+
},
1622+
],
1623+
},
14871624
],
14881625
invalid: [
14891626
{
@@ -7720,6 +7857,125 @@ const tests = {
77207857
},
77217858
],
77227859
},
7860+
{
7861+
code: normalizeIndent`
7862+
function MyComponent() {
7863+
const [stableItem, unstableItem] = useArrayWithStableItems();
7864+
useEffect(() => {
7865+
console.log(stableItem, unstableItem);
7866+
}, []);
7867+
}
7868+
`,
7869+
options: [
7870+
{
7871+
stableValueHooks: [
7872+
{
7873+
name: 'useArrayWithStableItems',
7874+
propertiesOrIndexes: [0],
7875+
},
7876+
],
7877+
},
7878+
],
7879+
errors: [
7880+
{
7881+
message:
7882+
/React Hook useEffect has a missing dependency: 'unstableItem'/,
7883+
suggestions: [
7884+
{
7885+
desc: 'Update the dependencies array to be: [unstableItem]',
7886+
output: normalizeIndent`
7887+
function MyComponent() {
7888+
const [stableItem, unstableItem] = useArrayWithStableItems();
7889+
useEffect(() => {
7890+
console.log(stableItem, unstableItem);
7891+
}, [unstableItem]);
7892+
}
7893+
`,
7894+
},
7895+
],
7896+
},
7897+
],
7898+
},
7899+
{
7900+
code: normalizeIndent`
7901+
function MyComponent() {
7902+
const [stableItem, unstableItem] = useArrayWithStableItems();
7903+
useEffect(() => {
7904+
console.log(stableItem, unstableItem);
7905+
}, []); // Missing unstableItem as dependency
7906+
}
7907+
`,
7908+
options: [
7909+
{
7910+
stableValueHooks: [
7911+
{
7912+
name: 'useArrayWithStableItems',
7913+
propertiesOrIndexes: [0],
7914+
},
7915+
],
7916+
},
7917+
],
7918+
errors: [
7919+
{
7920+
message:
7921+
"React Hook useEffect has a missing dependency: 'unstableItem'. " +
7922+
'Either include it or remove the dependency array.',
7923+
suggestions: [
7924+
{
7925+
desc: 'Update the dependencies array to be: [unstableItem]',
7926+
output: normalizeIndent`
7927+
function MyComponent() {
7928+
const [stableItem, unstableItem] = useArrayWithStableItems();
7929+
useEffect(() => {
7930+
console.log(stableItem, unstableItem);
7931+
}, [unstableItem]); // Missing unstableItem as dependency
7932+
}
7933+
`,
7934+
},
7935+
],
7936+
},
7937+
],
7938+
},
7939+
{
7940+
code: normalizeIndent`
7941+
function MyComponent() {
7942+
const { stableProp, unstableProp1, unstableProp2 } = useMixedStability();
7943+
useEffect(() => {
7944+
console.log(stableProp, unstableProp1, unstableProp2);
7945+
}, [unstableProp1]); // Missing unstableProp2 as dependency
7946+
}
7947+
`,
7948+
options: [
7949+
{
7950+
stableValueHooks: [
7951+
{
7952+
name: 'useMixedStability',
7953+
propertiesOrIndexes: ['stableProp'],
7954+
},
7955+
],
7956+
},
7957+
],
7958+
errors: [
7959+
{
7960+
message:
7961+
"React Hook useEffect has a missing dependency: 'unstableProp2'. " +
7962+
'Either include it or remove the dependency array.',
7963+
suggestions: [
7964+
{
7965+
desc: 'Update the dependencies array to be: [unstableProp1, unstableProp2]',
7966+
output: normalizeIndent`
7967+
function MyComponent() {
7968+
const { stableProp, unstableProp1, unstableProp2 } = useMixedStability();
7969+
useEffect(() => {
7970+
console.log(stableProp, unstableProp1, unstableProp2);
7971+
}, [unstableProp1, unstableProp2]); // Missing unstableProp2 as dependency
7972+
}
7973+
`,
7974+
},
7975+
],
7976+
},
7977+
],
7978+
},
77237979
],
77247980
};
77257981

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

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,39 @@ const rule = {
6767
type: 'string',
6868
},
6969
},
70+
stableValueHooks: {
71+
type: 'array',
72+
items: {
73+
type: 'object',
74+
properties: {
75+
name: {
76+
type: 'string',
77+
propertiesOrIndexes: {
78+
oneOf: [
79+
{
80+
type: 'null',
81+
},
82+
{
83+
type: 'array',
84+
items: {
85+
type: 'string',
86+
},
87+
},
88+
{
89+
type: 'array',
90+
items: {
91+
type: 'number',
92+
},
93+
},
94+
],
95+
},
96+
},
97+
},
98+
},
99+
},
70100
requireExplicitEffectDeps: {
71101
type: 'boolean',
72-
}
102+
},
73103
},
74104
},
75105
],
@@ -93,13 +123,23 @@ const rule = {
93123
? rawOptions.experimental_autoDependenciesHooks
94124
: [];
95125

96-
const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false;
126+
const requireExplicitEffectDeps: boolean =
127+
(rawOptions && rawOptions.requireExplicitEffectDeps) || false;
128+
129+
const stableValueHooks: Map<string, null | Array<number> | Array<string>> =
130+
new Map();
131+
if (rawOptions && rawOptions.stableValueHooks) {
132+
for (const config of rawOptions.stableValueHooks) {
133+
stableValueHooks.set(config.name, config.propertiesOrIndexes);
134+
}
135+
}
97136

98137
const options = {
99138
additionalHooks,
100139
experimental_autoDependenciesHooks,
101140
enableDangerousAutofixThisMayCauseInfiniteLoops,
102141
requireExplicitEffectDeps,
142+
stableValueHooks,
103143
};
104144

105145
function reportProblem(problem: Rule.ReportDescriptor) {
@@ -392,6 +432,54 @@ const rule = {
392432
return true;
393433
}
394434
}
435+
} else if (options.stableValueHooks.has(name)) {
436+
const config = options.stableValueHooks.get(name);
437+
if (config == null) {
438+
// No properties or indexes were provided, so the whole value is stable
439+
return id.type === 'Identifier';
440+
} else {
441+
/* An array of properties or indexes was provided.
442+
* We need to check if this variable is a destructured property or index
443+
* from the hook's return value.
444+
*/
445+
if (id.type === 'ArrayPattern') {
446+
// Find the index for the identifier we're checking
447+
const identifierIndex = id.elements.findIndex(
448+
el => el === resolved.identifiers[0],
449+
);
450+
451+
if (identifierIndex !== -1) {
452+
// Check if this index is in the configured list of stable indexes
453+
return (
454+
config.some(
455+
idx => idx === identifierIndex,
456+
)
457+
);
458+
}
459+
} else if (id.type === 'ObjectPattern') {
460+
// Find the destructured property for the identifier we're checking
461+
for (const property of id.properties) {
462+
if (
463+
property.type === 'Property' &&
464+
property.value.type === 'Identifier' &&
465+
property.value === resolved.identifiers[0] &&
466+
property.key.type === 'Identifier' &&
467+
'name' in property.key
468+
) {
469+
// Check if this property name is in the configured list of stable properties
470+
return (
471+
config.some(prop => {
472+
if ('name' in property.key) {
473+
return prop === property.key.name;
474+
}
475+
return false;
476+
})
477+
);
478+
}
479+
}
480+
}
481+
return false;
482+
}
395483
}
396484
// By default assume it's dynamic.
397485
return false;
@@ -1351,7 +1439,7 @@ const rule = {
13511439
node: reactiveHook,
13521440
message:
13531441
`React Hook ${reactiveHookName} always requires dependencies. ` +
1354-
`Please add a dependency array or an explicit \`undefined\``
1442+
`Please add a dependency array or an explicit \`undefined\``,
13551443
});
13561444
}
13571445

0 commit comments

Comments
 (0)