Skip to content

Commit 61a5566

Browse files
committed
feat(no-pass-live-state-to-parent): narrow check to passing state
#22
1 parent 5596e68 commit 61a5566

File tree

3 files changed

+56
-87
lines changed

3 files changed

+56
-87
lines changed

src/no-chain-state-updates.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ export const rule = {
5353
// But the upstream bit also catches intermediate variables that are ultimately literals.
5454
// We could either remove that and just check `arg.type === "Literal"`,
5555
// or could make this more readable by returning 'literal' | 'internal' | 'external' from the util functions...
56-
const argsRefs = callExpr.arguments.flatMap((arg) =>
57-
getDownstreamRefs(context, arg),
58-
);
59-
const argsUpstreamVariables = argsRefs.flatMap((ref) =>
60-
getUpstreamReactVariables(context, ref.identifier),
61-
);
56+
57+
const argsUpstreamVariables = callExpr.arguments
58+
.flatMap((arg) => getDownstreamRefs(context, arg))
59+
.flatMap((ref) =>
60+
getUpstreamReactVariables(context, ref.identifier),
61+
);
6262
const isAllArgsInternal = argsUpstreamVariables.notEmptyEvery(
6363
(variable) =>
6464
isState(variable) || (isProp(variable) && !isHOCProp(variable)),

src/no-pass-live-state-to-parent.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
isDirectCall,
77
isPropCallback,
88
isHOCProp,
9+
getUpstreamReactVariables,
10+
isState,
911
} from "./util/react.js";
10-
import { getCallExpr } from "./util/ast.js";
12+
import { getCallExpr, getDownstreamRefs } from "./util/ast.js";
1113

1214
export const name = "no-pass-live-state-to-parent";
1315
export const messages = {
@@ -24,7 +26,7 @@ export const rule = {
2426
schema: [],
2527
messages: {
2628
[messages.avoidPassingLiveStateToParent]:
27-
"Avoid passing live state to parent components in an effect. Instead, lift the state to the parent component and pass it down as a prop.",
29+
"Avoid passing live state to parents in an effect. Instead, lift the state to the parent and pass it down to the child as a prop.",
2830
},
2931
},
3032
create: (context) => ({
@@ -40,11 +42,17 @@ export const rule = {
4042
.filter(
4143
(ref) => isPropCallback(context, ref) && !isHOCProp(ref.resolved),
4244
)
43-
.filter((ref) => getCallExpr(ref).arguments.length > 0)
4445
.forEach((ref) => {
4546
const callExpr = getCallExpr(ref);
46-
// TODO: Unsure whether we should care about other things, like whether they're in deps...
47-
if (callExpr.arguments.some((arg) => arg.type !== "Literal")) {
47+
const argsUpstreamVariables = callExpr.arguments
48+
.flatMap((arg) => getDownstreamRefs(context, arg))
49+
.flatMap((ref) =>
50+
getUpstreamReactVariables(context, ref.identifier),
51+
);
52+
53+
if (
54+
argsUpstreamVariables.notEmptyEvery((variable) => isState(variable))
55+
) {
4856
context.report({
4957
node: callExpr,
5058
messageId: messages.avoidPassingLiveStateToParent,

test/no-pass-live-state-to-parent.test.js

Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@ new MyRuleTester().run(name, rule, {
1313
}
1414
`,
1515
},
16+
{
17+
name: "Pass live external state",
18+
code: js`
19+
const Child = ({ onFetched }) => {
20+
const data = useSomeAPI();
21+
22+
useEffect(() => {
23+
onFetched(data);
24+
}, [onFetched, data]);
25+
}
26+
`,
27+
},
28+
{
29+
// No idea why someone would do this, so we only check for passing state, but maybe there's a less contrived pattern.
30+
// `no-manage-parent` would catch this anyway.
31+
name: "Pass prop to parent",
32+
code: js`
33+
const Child = ({ text, onTextChanged }) => {
34+
useEffect(() => {
35+
onTextChanged(text);
36+
}, [text, onTextChanged]);
37+
}
38+
`,
39+
},
1640
{
1741
name: "No-arg prop callback in response to internal state change",
1842
code: js`
@@ -80,23 +104,6 @@ new MyRuleTester().run(name, rule, {
80104
},
81105
],
82106
invalid: [
83-
{
84-
name: "Pass live internal fetched state",
85-
code: js`
86-
const Child = ({ onFetched }) => {
87-
const [data, setData] = useState();
88-
89-
useEffect(() => {
90-
onFetched(data);
91-
}, [onFetched, data]);
92-
}
93-
`,
94-
errors: [
95-
{
96-
messageId: messages.avoidPassingLiveStateToParent,
97-
},
98-
],
99-
},
100107
{
101108
name: "Pass live internal state",
102109
code: js`
@@ -124,13 +131,20 @@ new MyRuleTester().run(name, rule, {
124131
{
125132
name: "Pass live derived internal state",
126133
code: js`
127-
const Child = ({ onFetched }) => {
128-
const [data, setData] = useState();
134+
const Child = ({ onTextChanged }) => {
135+
const [text, setText] = useState();
129136
130137
useEffect(() => {
131-
const firstElement = data[0];
132-
onFetched(firstElement);
133-
}, [onFetched, data]);
138+
const firstChar = text[0];
139+
onTextChanged(firstChar);
140+
}, [onTextChanged, text]);
141+
142+
return (
143+
<input
144+
type="text"
145+
onChange={(e) => setText(e.target.value)}
146+
/>
147+
);
134148
}
135149
`,
136150
errors: [
@@ -159,42 +173,7 @@ new MyRuleTester().run(name, rule, {
159173
],
160174
},
161175
{
162-
name: "Pass live external state",
163-
code: js`
164-
const Child = ({ onFetched }) => {
165-
const data = useSomeAPI();
166-
167-
useEffect(() => {
168-
onFetched(data);
169-
}, [onFetched, data]);
170-
}
171-
`,
172-
errors: [
173-
{
174-
messageId: messages.avoidPassingLiveStateToParent,
175-
},
176-
],
177-
},
178-
{
179-
name: "Pass derived live external state",
180-
code: js`
181-
const Child = ({ onFetched }) => {
182-
const data = useSomeAPI();
183-
const firstElement = data[0];
184-
185-
useEffect(() => {
186-
onFetched(firstElement);
187-
}, [onFetched, firstElement]);
188-
}
189-
`,
190-
errors: [
191-
{
192-
messageId: messages.avoidPassingLiveStateToParent,
193-
},
194-
],
195-
},
196-
{
197-
name: "Pass final external state",
176+
name: "Pass final internal state",
198177
code: js`
199178
function Form({ onSubmit }) {
200179
const [name, setName] = useState();
@@ -224,23 +203,5 @@ new MyRuleTester().run(name, rule, {
224203
},
225204
],
226205
},
227-
{
228-
name: "From props via member function",
229-
code: js`
230-
function DoubleList({ list }) {
231-
const [doubleList, setDoubleList] = useState([]);
232-
233-
useEffect(() => {
234-
setDoubleList(list.concat(list));
235-
}, [list]);
236-
}
237-
`,
238-
errors: [
239-
{
240-
// We consider `list.concat` to essentially be a prop callback
241-
messageId: messages.avoidPassingLiveStateToParent,
242-
},
243-
],
244-
},
245206
],
246207
});

0 commit comments

Comments
 (0)