Skip to content

Commit bc82f15

Browse files
committed
fix(no-derived-state): account for non-calls when flagging single setter calls
1 parent da144f5 commit bc82f15

File tree

4 files changed

+20
-49
lines changed

4 files changed

+20
-49
lines changed

src/no-derived-state.js

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
isHOCProp,
1111
getUpstreamReactVariables,
1212
isState,
13+
countCalls,
1314
} from "./util/react.js";
1415
import { getCallExpr, getDownstreamRefs } from "./util/ast.js";
15-
import { arraysEqual } from "./util/javascript.js";
1616

1717
export const name = "no-derived-state";
1818
export const messages = {
@@ -48,36 +48,26 @@ export const rule = {
4848
const stateName = (
4949
useStateNode.id.elements[0] ?? useStateNode.id.elements[1]
5050
)?.name;
51-
const argsRefs = callExpr.arguments.flatMap((arg) =>
52-
getDownstreamRefs(context, arg),
53-
);
54-
const argsUpstreamVariables = argsRefs.flatMap((ref) =>
51+
52+
const argsUpstreamVars = callExpr.arguments
53+
.flatMap((arg) => getDownstreamRefs(context, arg))
54+
.flatMap((ref) => getUpstreamReactVariables(context, ref.resolved));
55+
const depsUpstreamVars = depsRefs.flatMap((ref) =>
5556
getUpstreamReactVariables(context, ref.resolved),
5657
);
57-
const isAllArgsInternal = argsUpstreamVariables.notEmptyEvery(
58+
const isAllArgsInternal = argsUpstreamVars.notEmptyEvery(
5859
(variable) =>
5960
isState(variable) || (isProp(variable) && !isHOCProp(variable)),
6061
);
61-
const isAllArgsInDeps = argsRefs
62-
.filter((ref) =>
63-
ref.resolved.defs.every((def) => def.type !== "Parameter"),
64-
)
65-
.notEmptyEvery((argRef) =>
66-
depsRefs.some((depRef) =>
67-
arraysEqual(
68-
getUpstreamReactVariables(context, argRef.resolved),
69-
getUpstreamReactVariables(context, depRef.resolved),
70-
),
71-
),
72-
);
73-
// In this case the derived state will always be in sync,
74-
// thus it could be computed directly during render
75-
const isStateSetterCalledOnce =
76-
ref.resolved.references.length - 1 === 1;
62+
const isAllArgsInDeps = argsUpstreamVars.notEmptyEvery((argVar) =>
63+
depsUpstreamVars.some((depVar) => argVar.name === depVar.name),
64+
);
7765

7866
if (
7967
isAllArgsInternal ||
80-
(isAllArgsInDeps && isStateSetterCalledOnce)
68+
// In this case the derived state will always be in sync,
69+
// thus it could be computed directly during render
70+
(isAllArgsInDeps && countCalls(ref) === 1)
8171
) {
8272
context.report({
8373
node: callExpr,

src/util/javascript.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
11
Array.prototype.notEmptyEvery = function (predicate) {
22
return this.length > 0 && this.every(predicate);
33
};
4-
5-
export const arraysEqual = (arr1, arr2) => {
6-
if (arr1.length !== arr2.length) {
7-
return false;
8-
}
9-
return arr1.every((element, index) => element === arr2[index]);
10-
};

src/util/react.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ const countUseStates = (context, componentNode) => {
242242
return count;
243243
};
244244

245-
export const countStateSetterCalls = (stateSetterRef) =>
246-
stateSetterRef.resolved.references.length - 1; // -1 for the initial declaration
245+
export const countCalls = (ref) =>
246+
ref.resolved.references.filter(
247+
(ref) => ref.identifier.parent.type === "CallExpression",
248+
).length;
247249

248250
// Returns the component or custom hook that contains the `useEffect` node
249251
const findContainingNode = (node) => {

test/no-derived-state.test.js

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,6 @@ new MyRuleTester().run(name, rule, {
147147
}
148148
`,
149149
},
150-
{
151-
name: "From external state via member function",
152-
code: js`
153-
function Counter() {
154-
const countGetter = useSomeAPI();
155-
const [count, setCount] = useState(0);
156-
157-
useEffect(() => {
158-
const newCount = countGetter.getCount();
159-
setCount(newCount);
160-
}, [countGetter, setCount]);
161-
}
162-
`,
163-
},
164150
{
165151
name: "Via pure local function",
166152
code: js`
@@ -445,7 +431,7 @@ new MyRuleTester().run(name, rule, {
445431
446432
useEffect(() => {
447433
setSelectedPost(posts[0]);
448-
}, [posts]);
434+
}, [posts, setSelectedPost]);
449435
}
450436
`,
451437
errors: [
@@ -465,7 +451,7 @@ new MyRuleTester().run(name, rule, {
465451
useEffect(() => {
466452
const prefixedName = 'Dr. ' + name;
467453
setFullName(prefixedName)
468-
}, [name]);
454+
}, [name, setFullName]);
469455
}
470456
`,
471457
errors: [
@@ -486,7 +472,7 @@ new MyRuleTester().run(name, rule, {
486472
487473
useEffect(() => {
488474
setLocation(history.location);
489-
}, [history.location]);
475+
}, [history.location, setLocation]);
490476
});
491477
`,
492478
errors: [

0 commit comments

Comments
 (0)