Skip to content

Commit a77bbf1

Browse files
authored
[ESLint] Warn against assignments from inside Hooks (#14916)
* [ESLint] Warn against assignments from inside Hooks * Include variable name * Add a test for the legit case
1 parent 219ce8a commit a77bbf1

File tree

2 files changed

+161
-3
lines changed

2 files changed

+161
-3
lines changed

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

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,19 @@ const tests = {
433433
});
434434
`,
435435
},
436+
{
437+
// This is not ideal but warning would likely create
438+
// too many false positives. We do, however, prevent
439+
// direct assignments.
440+
code: `
441+
function MyComponent(props) {
442+
let obj = {};
443+
useEffect(() => {
444+
obj.foo = true;
445+
}, [obj]);
446+
}
447+
`,
448+
},
436449
],
437450
invalid: [
438451
{
@@ -891,7 +904,6 @@ const tests = {
891904
],
892905
},
893906
{
894-
// only: true,
895907
code: `
896908
function MyComponent(props) {
897909
useEffect(() => {
@@ -1608,6 +1620,128 @@ const tests = {
16081620
'Either include them or remove the dependency array.',
16091621
],
16101622
},
1623+
{
1624+
code: `
1625+
function MyComponent(props) {
1626+
let value;
1627+
let value2;
1628+
let value3;
1629+
let asyncValue;
1630+
useEffect(() => {
1631+
value = 42;
1632+
value2 = 100;
1633+
value = 43;
1634+
console.log(value2);
1635+
console.log(value3);
1636+
setTimeout(() => {
1637+
asyncValue = 100;
1638+
});
1639+
}, []);
1640+
}
1641+
`,
1642+
// This is a separate warning unrelated to others.
1643+
// We could've made a separate rule for it but it's rare enough to name it.
1644+
// No autofix suggestion because the intent isn't clear.
1645+
output: `
1646+
function MyComponent(props) {
1647+
let value;
1648+
let value2;
1649+
let value3;
1650+
let asyncValue;
1651+
useEffect(() => {
1652+
value = 42;
1653+
value2 = 100;
1654+
value = 43;
1655+
console.log(value2);
1656+
console.log(value3);
1657+
setTimeout(() => {
1658+
asyncValue = 100;
1659+
});
1660+
}, []);
1661+
}
1662+
`,
1663+
errors: [
1664+
// value
1665+
`Assignments to the 'value' variable from inside a React useEffect Hook ` +
1666+
`will not persist between re-renders. ` +
1667+
`If it's only needed by this Hook, move the variable inside it. ` +
1668+
`Alternatively, declare a ref with the useRef Hook, ` +
1669+
`and keep the mutable value in its 'current' property.`,
1670+
// value2
1671+
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
1672+
`will not persist between re-renders. ` +
1673+
`If it's only needed by this Hook, move the variable inside it. ` +
1674+
`Alternatively, declare a ref with the useRef Hook, ` +
1675+
`and keep the mutable value in its 'current' property.`,
1676+
// asyncValue
1677+
`Assignments to the 'asyncValue' variable from inside a React useEffect Hook ` +
1678+
`will not persist between re-renders. ` +
1679+
`If it's only needed by this Hook, move the variable inside it. ` +
1680+
`Alternatively, declare a ref with the useRef Hook, ` +
1681+
`and keep the mutable value in its 'current' property.`,
1682+
],
1683+
},
1684+
{
1685+
code: `
1686+
function MyComponent(props) {
1687+
let value;
1688+
let value2;
1689+
let value3;
1690+
let asyncValue;
1691+
useEffect(() => {
1692+
value = 42;
1693+
value2 = 100;
1694+
value = 43;
1695+
console.log(value2);
1696+
console.log(value3);
1697+
setTimeout(() => {
1698+
asyncValue = 100;
1699+
});
1700+
}, [value, value2, value3]);
1701+
}
1702+
`,
1703+
// This is a separate warning unrelated to others.
1704+
// We could've made a separate rule for it but it's rare enough to name it.
1705+
// No autofix suggestion because the intent isn't clear.
1706+
output: `
1707+
function MyComponent(props) {
1708+
let value;
1709+
let value2;
1710+
let value3;
1711+
let asyncValue;
1712+
useEffect(() => {
1713+
value = 42;
1714+
value2 = 100;
1715+
value = 43;
1716+
console.log(value2);
1717+
console.log(value3);
1718+
setTimeout(() => {
1719+
asyncValue = 100;
1720+
});
1721+
}, [value, value2, value3]);
1722+
}
1723+
`,
1724+
errors: [
1725+
// value
1726+
`Assignments to the 'value' variable from inside a React useEffect Hook ` +
1727+
`will not persist between re-renders. ` +
1728+
`If it's only needed by this Hook, move the variable inside it. ` +
1729+
`Alternatively, declare a ref with the useRef Hook, ` +
1730+
`and keep the mutable value in its 'current' property.`,
1731+
// value2
1732+
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
1733+
`will not persist between re-renders. ` +
1734+
`If it's only needed by this Hook, move the variable inside it. ` +
1735+
`Alternatively, declare a ref with the useRef Hook, ` +
1736+
`and keep the mutable value in its 'current' property.`,
1737+
// asyncValue
1738+
`Assignments to the 'asyncValue' variable from inside a React useEffect Hook ` +
1739+
`will not persist between re-renders. ` +
1740+
`If it's only needed by this Hook, move the variable inside it. ` +
1741+
`Alternatively, declare a ref with the useRef Hook, ` +
1742+
`and keep the mutable value in its 'current' property.`,
1743+
],
1744+
},
16111745
],
16121746
};
16131747

packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ export default {
186186
// again in our dependencies array. Remember whether it's static.
187187
if (!dependencies.has(dependency)) {
188188
const isStatic = isDefinitelyStaticDependency(reference);
189-
dependencies.set(dependency, isStatic);
189+
dependencies.set(dependency, {
190+
isStatic,
191+
reference,
192+
});
190193
}
191194
}
192195
for (const childScope of currentScope.childScopes) {
@@ -258,6 +261,7 @@ export default {
258261
let unnecessaryDependencies = new Set();
259262
let missingDependencies = new Set();
260263
let actualDependencies = Array.from(dependencies.keys());
264+
let foundStaleAssignments = false;
261265

262266
function satisfies(actualDep, dep) {
263267
return actualDep === dep || actualDep.startsWith(dep + '.');
@@ -280,7 +284,22 @@ export default {
280284
});
281285

282286
// Then fill in the missing ones.
283-
dependencies.forEach((isStatic, key) => {
287+
dependencies.forEach(({isStatic, reference}, key) => {
288+
if (reference.writeExpr) {
289+
foundStaleAssignments = true;
290+
context.report({
291+
node: reference.writeExpr,
292+
message:
293+
`Assignments to the '${key}' variable from inside a React ${context.getSource(
294+
reactiveHook,
295+
)} Hook ` +
296+
`will not persist between re-renders. ` +
297+
`If it's only needed by this Hook, move the variable inside it. ` +
298+
`Alternatively, declare a ref with the useRef Hook, ` +
299+
`and keep the mutable value in its 'current' property.`,
300+
});
301+
}
302+
284303
if (
285304
!suggestedDependencies.some(suggestedDep =>
286305
satisfies(key, suggestedDep),
@@ -319,6 +338,11 @@ export default {
319338
return;
320339
}
321340

341+
if (foundStaleAssignments) {
342+
// The intent isn't clear so we'll wait until you fix those first.
343+
return;
344+
}
345+
322346
function getWarningMessage(deps, singlePrefix, label, fixVerb) {
323347
if (deps.size === 0) {
324348
return null;

0 commit comments

Comments
 (0)