Skip to content

Commit

Permalink
[ESLint] Warn against assignments from inside Hooks (#14916)
Browse files Browse the repository at this point in the history
* [ESLint] Warn against assignments from inside Hooks

* Include variable name

* Add a test for the legit case
  • Loading branch information
gaearon authored Feb 21, 2019
1 parent 219ce8a commit a77bbf1
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,19 @@ const tests = {
});
`,
},
{
// This is not ideal but warning would likely create
// too many false positives. We do, however, prevent
// direct assignments.
code: `
function MyComponent(props) {
let obj = {};
useEffect(() => {
obj.foo = true;
}, [obj]);
}
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -891,7 +904,6 @@ const tests = {
],
},
{
// only: true,
code: `
function MyComponent(props) {
useEffect(() => {
Expand Down Expand Up @@ -1608,6 +1620,128 @@ const tests = {
'Either include them or remove the dependency array.',
],
},
{
code: `
function MyComponent(props) {
let value;
let value2;
let value3;
let asyncValue;
useEffect(() => {
value = 42;
value2 = 100;
value = 43;
console.log(value2);
console.log(value3);
setTimeout(() => {
asyncValue = 100;
});
}, []);
}
`,
// This is a separate warning unrelated to others.
// We could've made a separate rule for it but it's rare enough to name it.
// No autofix suggestion because the intent isn't clear.
output: `
function MyComponent(props) {
let value;
let value2;
let value3;
let asyncValue;
useEffect(() => {
value = 42;
value2 = 100;
value = 43;
console.log(value2);
console.log(value3);
setTimeout(() => {
asyncValue = 100;
});
}, []);
}
`,
errors: [
// value
`Assignments to the 'value' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// value2
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// asyncValue
`Assignments to the 'asyncValue' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
],
},
{
code: `
function MyComponent(props) {
let value;
let value2;
let value3;
let asyncValue;
useEffect(() => {
value = 42;
value2 = 100;
value = 43;
console.log(value2);
console.log(value3);
setTimeout(() => {
asyncValue = 100;
});
}, [value, value2, value3]);
}
`,
// This is a separate warning unrelated to others.
// We could've made a separate rule for it but it's rare enough to name it.
// No autofix suggestion because the intent isn't clear.
output: `
function MyComponent(props) {
let value;
let value2;
let value3;
let asyncValue;
useEffect(() => {
value = 42;
value2 = 100;
value = 43;
console.log(value2);
console.log(value3);
setTimeout(() => {
asyncValue = 100;
});
}, [value, value2, value3]);
}
`,
errors: [
// value
`Assignments to the 'value' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// value2
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// asyncValue
`Assignments to the 'asyncValue' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
],
},
],
};

Expand Down
28 changes: 26 additions & 2 deletions packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ export default {
// again in our dependencies array. Remember whether it's static.
if (!dependencies.has(dependency)) {
const isStatic = isDefinitelyStaticDependency(reference);
dependencies.set(dependency, isStatic);
dependencies.set(dependency, {
isStatic,
reference,
});
}
}
for (const childScope of currentScope.childScopes) {
Expand Down Expand Up @@ -258,6 +261,7 @@ export default {
let unnecessaryDependencies = new Set();
let missingDependencies = new Set();
let actualDependencies = Array.from(dependencies.keys());
let foundStaleAssignments = false;

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

// Then fill in the missing ones.
dependencies.forEach((isStatic, key) => {
dependencies.forEach(({isStatic, reference}, key) => {
if (reference.writeExpr) {
foundStaleAssignments = true;
context.report({
node: reference.writeExpr,
message:
`Assignments to the '${key}' variable from inside a React ${context.getSource(
reactiveHook,
)} Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
});
}

if (
!suggestedDependencies.some(suggestedDep =>
satisfies(key, suggestedDep),
Expand Down Expand Up @@ -319,6 +338,11 @@ export default {
return;
}

if (foundStaleAssignments) {
// The intent isn't clear so we'll wait until you fix those first.
return;
}

function getWarningMessage(deps, singlePrefix, label, fixVerb) {
if (deps.size === 0) {
return null;
Expand Down

0 comments on commit a77bbf1

Please sign in to comment.