Skip to content

Commit 08edb7c

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

File tree

3 files changed

+74
-82
lines changed

3 files changed

+74
-82
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: 11 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,15 @@ 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 (argsUpstreamVariables.some((variable) => isState(variable))) {
4854
context.report({
4955
node: callExpr,
5056
messageId: messages.avoidPassingLiveStateToParent,

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

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,31 @@ new MyRuleTester().run(name, rule, {
1414
`,
1515
},
1616
{
17-
name: "No-arg prop callback in response to internal state change",
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+
},
40+
{
41+
name: "No-arg prop callback",
1842
code: js`
1943
function Form({ onClose }) {
2044
const [name, setName] = useState();
@@ -49,7 +73,7 @@ new MyRuleTester().run(name, rule, {
4973
`,
5074
},
5175
{
52-
name: "Prop from library HOC used internally",
76+
name: "Pass internal state to HOC prop",
5377
code: js`
5478
import { withRouter } from 'react-router-dom';
5579
@@ -63,7 +87,7 @@ new MyRuleTester().run(name, rule, {
6387
`,
6488
},
6589
{
66-
name: "Prop from library HOC used externally",
90+
name: "Pass external state to HOC prop",
6791
code: js`
6892
import { withRouter } from 'react-router-dom';
6993
@@ -72,7 +96,7 @@ new MyRuleTester().run(name, rule, {
7296
7397
useEffect(() => {
7498
if (data.error) {
75-
history.push('/error-page');
99+
history.push(data.error);
76100
}
77101
}, [data]);
78102
});
@@ -81,14 +105,21 @@ new MyRuleTester().run(name, rule, {
81105
],
82106
invalid: [
83107
{
84-
name: "Pass live internal fetched state",
108+
name: "Pass live internal state",
85109
code: js`
86-
const Child = ({ onFetched }) => {
87-
const [data, setData] = useState();
110+
const Child = ({ onTextChanged }) => {
111+
const [text, setText] = useState();
88112
89113
useEffect(() => {
90-
onFetched(data);
91-
}, [onFetched, data]);
114+
onTextChanged(text);
115+
}, [onTextChanged, text]);
116+
117+
return (
118+
<input
119+
type="text"
120+
onChange={(e) => setText(e.target.value)}
121+
/>
122+
);
92123
}
93124
`,
94125
errors: [
@@ -98,14 +129,15 @@ new MyRuleTester().run(name, rule, {
98129
],
99130
},
100131
{
101-
name: "Pass live internal state",
132+
name: "Pass live internal state AND external state",
102133
code: js`
103134
const Child = ({ onTextChanged }) => {
104135
const [text, setText] = useState();
136+
const data = useSomeAPI();
105137
106138
useEffect(() => {
107-
onTextChanged(text);
108-
}, [onTextChanged, text]);
139+
onTextChanged(text, data);
140+
}, [onTextChanged, text, data]);
109141
110142
return (
111143
<input
@@ -124,13 +156,20 @@ new MyRuleTester().run(name, rule, {
124156
{
125157
name: "Pass live derived internal state",
126158
code: js`
127-
const Child = ({ onFetched }) => {
128-
const [data, setData] = useState();
159+
const Child = ({ onTextChanged }) => {
160+
const [text, setText] = useState();
129161
130162
useEffect(() => {
131-
const firstElement = data[0];
132-
onFetched(firstElement);
133-
}, [onFetched, data]);
163+
const firstChar = text[0];
164+
onTextChanged(firstChar);
165+
}, [onTextChanged, text]);
166+
167+
return (
168+
<input
169+
type="text"
170+
onChange={(e) => setText(e.target.value)}
171+
/>
172+
);
134173
}
135174
`,
136175
errors: [
@@ -159,42 +198,7 @@ new MyRuleTester().run(name, rule, {
159198
],
160199
},
161200
{
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",
201+
name: "Pass final internal state",
198202
code: js`
199203
function Form({ onSubmit }) {
200204
const [name, setName] = useState();
@@ -224,23 +228,5 @@ new MyRuleTester().run(name, rule, {
224228
},
225229
],
226230
},
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-
},
245231
],
246232
});

0 commit comments

Comments
 (0)