Skip to content

Commit 948bf95

Browse files
potetojorge-cab
authored andcommitted
[compiler][wip] Extend ValidateNoDerivedComputationsInEffects for props derived effects
This PR adds infra to disambiguate between two types of derived state in effects: 1. State derived from props 2. State derived from other state TODO: - [ ] Props tracking through destructuring and property access does not seem to be propagated correctly inside of Functions' instructions (or i might be misunderstanding how we track aliasing effects) - [ ] compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js should be failing - [ ] Handle "mixed" case where deps flow from at least one prop AND state. Should probably have a different error reason, to aid with categorization
1 parent 030a610 commit 948bf95

7 files changed

+194
-27
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts

Lines changed: 171 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,21 @@ import {
1313
FunctionExpression,
1414
HIRFunction,
1515
IdentifierId,
16+
Place,
1617
isSetStateType,
1718
isUseEffectHookType,
1819
} from '../HIR';
20+
import {printInstruction, printPlace} from '../HIR/PrintHIR';
1921
import {
2022
eachInstructionValueOperand,
2123
eachTerminalOperand,
2224
} from '../HIR/visitors';
2325

26+
type SetStateCall = {
27+
loc: SourceLocation;
28+
propsSource: Place | null; // null means state-derived, non-null means props-derived
29+
};
30+
2431
/**
2532
* Validates that useEffect is not used for derived computations which could/should
2633
* be performed in render.
@@ -48,12 +55,96 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
4855
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
4956
const functions: Map<IdentifierId, FunctionExpression> = new Map();
5057
const locals: Map<IdentifierId, IdentifierId> = new Map();
58+
const derivedFromProps: Map<IdentifierId, Place> = new Map();
5159

5260
const errors = new CompilerError();
5361

62+
if (fn.fnType === 'Hook') {
63+
for (const param of fn.params) {
64+
if (param.kind === 'Identifier') {
65+
derivedFromProps.set(param.identifier.id, param);
66+
}
67+
}
68+
} else if (fn.fnType === 'Component') {
69+
const props = fn.params[0];
70+
if (props != null && props.kind === 'Identifier') {
71+
derivedFromProps.set(props.identifier.id, props);
72+
}
73+
}
74+
5475
for (const block of fn.body.blocks.values()) {
5576
for (const instr of block.instructions) {
5677
const {lvalue, value} = instr;
78+
79+
// Track props derivation through instruction effects
80+
if (instr.effects != null) {
81+
for (const effect of instr.effects) {
82+
switch (effect.kind) {
83+
case 'Assign':
84+
case 'Alias':
85+
case 'MaybeAlias':
86+
case 'Capture': {
87+
const source = derivedFromProps.get(effect.from.identifier.id);
88+
if (source != null) {
89+
derivedFromProps.set(effect.into.identifier.id, source);
90+
}
91+
break;
92+
}
93+
}
94+
}
95+
}
96+
97+
/**
98+
* TODO: figure out why property access off of props does not create an Assign or Alias/Maybe
99+
* Alias
100+
*
101+
* import {useEffect, useState} from 'react'
102+
*
103+
* function Component(props) {
104+
* const [displayValue, setDisplayValue] = useState('');
105+
*
106+
* useEffect(() => {
107+
* const computed = props.prefix + props.value + props.suffix;
108+
* ^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^
109+
* we want to track that these are from props
110+
* setDisplayValue(computed);
111+
* }, [props.prefix, props.value, props.suffix]);
112+
*
113+
* return <div>{displayValue}</div>;
114+
* }
115+
*/
116+
if (value.kind === 'FunctionExpression') {
117+
for (const [, block] of value.loweredFunc.func.body.blocks) {
118+
for (const instr of block.instructions) {
119+
if (instr.effects != null) {
120+
console.group(printInstruction(instr));
121+
for (const effect of instr.effects) {
122+
console.log(effect);
123+
switch (effect.kind) {
124+
case 'Assign':
125+
case 'Alias':
126+
case 'MaybeAlias':
127+
case 'Capture': {
128+
const source = derivedFromProps.get(
129+
effect.from.identifier.id,
130+
);
131+
if (source != null) {
132+
derivedFromProps.set(effect.into.identifier.id, source);
133+
}
134+
break;
135+
}
136+
}
137+
}
138+
}
139+
console.groupEnd();
140+
}
141+
}
142+
}
143+
144+
for (const [, place] of derivedFromProps) {
145+
console.log(printPlace(place));
146+
}
147+
57148
if (value.kind === 'LoadLocal') {
58149
locals.set(lvalue.identifier.id, value.place.identifier.id);
59150
} else if (value.kind === 'ArrayExpression') {
@@ -90,6 +181,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
90181
validateEffect(
91182
effectFunction.loweredFunc.func,
92183
dependencies,
184+
derivedFromProps,
93185
errors,
94186
);
95187
}
@@ -105,35 +197,49 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
105197
function validateEffect(
106198
effectFunction: HIRFunction,
107199
effectDeps: Array<IdentifierId>,
200+
derivedFromProps: Map<IdentifierId, Place>,
108201
errors: CompilerError,
109202
): void {
110203
for (const operand of effectFunction.context) {
111204
if (isSetStateType(operand.identifier)) {
112205
continue;
113206
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
114207
continue;
208+
} else if (derivedFromProps.has(operand.identifier.id)) {
209+
continue;
115210
} else {
116211
// Captured something other than the effect dep or setState
212+
console.log('early return 1');
117213
return;
118214
}
119215
}
120216
for (const dep of effectDeps) {
217+
console.log({dep});
121218
if (
122219
effectFunction.context.find(operand => operand.identifier.id === dep) ==
123-
null
220+
null ||
221+
derivedFromProps.has(dep) === false
124222
) {
223+
console.log('early return 2');
125224
// effect dep wasn't actually used in the function
126225
return;
127226
}
128227
}
129228

130229
const seenBlocks: Set<BlockId> = new Set();
131230
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
231+
const effectDerivedFromProps: Map<IdentifierId, Place> = new Map();
232+
132233
for (const dep of effectDeps) {
234+
console.log({dep});
133235
values.set(dep, [dep]);
236+
const propsSource = derivedFromProps.get(dep);
237+
if (propsSource != null) {
238+
effectDerivedFromProps.set(dep, propsSource);
239+
}
134240
}
135241

136-
const setStateLocations: Array<SourceLocation> = [];
242+
const setStateCalls: Array<SetStateCall> = [];
137243
for (const block of effectFunction.body.blocks.values()) {
138244
for (const pred of block.preds) {
139245
if (!seenBlocks.has(pred)) {
@@ -143,17 +249,27 @@ function validateEffect(
143249
}
144250
for (const phi of block.phis) {
145251
const aggregateDeps: Set<IdentifierId> = new Set();
252+
let propsSource: Place | null = null;
253+
146254
for (const operand of phi.operands.values()) {
147255
const deps = values.get(operand.identifier.id);
148256
if (deps != null) {
149257
for (const dep of deps) {
150258
aggregateDeps.add(dep);
151259
}
152260
}
261+
const source = effectDerivedFromProps.get(operand.identifier.id);
262+
if (source != null) {
263+
propsSource = source;
264+
}
153265
}
266+
154267
if (aggregateDeps.size !== 0) {
155268
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
156269
}
270+
if (propsSource != null) {
271+
effectDerivedFromProps.set(phi.place.identifier.id, propsSource);
272+
}
157273
}
158274
for (const instr of block.instructions) {
159275
switch (instr.value.kind) {
@@ -196,9 +312,16 @@ function validateEffect(
196312
) {
197313
const deps = values.get(instr.value.args[0].identifier.id);
198314
if (deps != null && new Set(deps).size === effectDeps.length) {
199-
setStateLocations.push(instr.value.callee.loc);
315+
const propsSource = effectDerivedFromProps.get(
316+
instr.value.args[0].identifier.id,
317+
);
318+
319+
setStateCalls.push({
320+
loc: instr.value.callee.loc,
321+
propsSource: propsSource ?? null,
322+
});
200323
} else {
201-
// doesn't depend on any deps
324+
// doesn't depend on all deps
202325
return;
203326
}
204327
}
@@ -208,6 +331,26 @@ function validateEffect(
208331
return;
209332
}
210333
}
334+
335+
// Track props derivation through instruction effects
336+
if (instr.effects != null) {
337+
for (const effect of instr.effects) {
338+
switch (effect.kind) {
339+
case 'Assign':
340+
case 'Alias':
341+
case 'MaybeAlias':
342+
case 'Capture': {
343+
const source = effectDerivedFromProps.get(
344+
effect.from.identifier.id,
345+
);
346+
if (source != null) {
347+
effectDerivedFromProps.set(effect.into.identifier.id, source);
348+
}
349+
break;
350+
}
351+
}
352+
}
353+
}
211354
}
212355
for (const operand of eachTerminalOperand(block.terminal)) {
213356
if (values.has(operand.identifier.id)) {
@@ -218,15 +361,29 @@ function validateEffect(
218361
seenBlocks.add(block.id);
219362
}
220363

221-
for (const loc of setStateLocations) {
222-
errors.push({
223-
category: ErrorCategory.EffectDerivationsOfState,
224-
reason:
225-
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
226-
description: null,
227-
severity: ErrorSeverity.InvalidReact,
228-
loc,
229-
suggestions: null,
230-
});
364+
for (const call of setStateCalls) {
365+
if (call.propsSource != null) {
366+
const propName = call.propsSource.identifier.name?.value;
367+
const propInfo = propName != null ? ` (from prop '${propName}')` : '';
368+
369+
errors.push({
370+
reason: `Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)`,
371+
description: `You are using props${propInfo} to update local state in an effect.`,
372+
severity: ErrorSeverity.InvalidReact,
373+
loc: call.loc,
374+
suggestions: null,
375+
});
376+
} else {
377+
errors.push({
378+
reason:
379+
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
380+
description:
381+
'This effect updates state based on other state values. ' +
382+
'Consider calculating this value directly during render',
383+
severity: ErrorSeverity.InvalidReact,
384+
loc: call.loc,
385+
suggestions: null,
386+
});
387+
}
231388
}
232389
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ function BadExample() {
2424
```
2525
Found 1 error:
2626
27-
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
27+
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
28+
29+
This effect updates state based on other state values. Consider calculating this value directly during render.
2830
2931
error.invalid-derived-computation-in-effect.ts:9:4
3032
7 | const [fullName, setFullName] = useState('');
3133
8 | useEffect(() => {
3234
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
33-
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
35+
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
3436
10 | }, [firstName, lastName]);
3537
11 |
3638
12 | return <div>{fullName}</div>;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.expect.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ export const FIXTURE_ENTRYPOINT = {
3434
```
3535
Found 1 error:
3636
37-
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
37+
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
3838
39-
error.derived-state-from-mixed-deps-no-error.ts:9:4
39+
This effect updates state based on other state values. Consider calculating this value directly during render.
40+
41+
error.bug-derived-state-from-mixed-deps.ts:9:4
4042
7 |
4143
8 | useEffect(() => {
4244
> 9 | setDisplayName(prefix + name);
43-
| ^^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
45+
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
4446
10 | }, [prefix, name]);
4547
11 |
4648
12 | return (

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ export const FIXTURE_ENTRYPOINT = {
2828
```
2929
Found 1 error:
3030
31-
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
31+
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
32+
33+
This effect updates state based on other state values. Consider calculating this value directly during render.
3234
3335
error.invalid-derived-state-from-props-destructured.ts:8:4
3436
6 |
3537
7 | useEffect(() => {
3638
> 8 | setFullName(firstName + ' ' + lastName);
37-
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
39+
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
3840
9 | }, [firstName, lastName]);
3941
10 |
4042
11 | return <div>{fullName}</div>;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @validateNoDerivedComputationsInEffects
22
import {useEffect, useState} from 'react';
33

4-
function Component({user: {firstName, lastName}}) {
4+
function Component({firstName, lastName}) {
55
const [fullName, setFullName] = useState('');
66

77
useEffect(() => {
@@ -13,5 +13,5 @@ function Component({user: {firstName, lastName}}) {
1313

1414
export const FIXTURE_ENTRYPOINT = {
1515
fn: Component,
16-
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
16+
params: [{firstName: 'John', lastName: 'Doe'}],
1717
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ export const FIXTURE_ENTRYPOINT = {
2828
```
2929
Found 1 error:
3030
31-
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
31+
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
32+
33+
This effect updates state based on other state values. Consider calculating this value directly during render.
3234
3335
error.invalid-derived-state-from-props-in-effect.ts:8:4
3436
6 |
3537
7 | useEffect(() => {
3638
> 8 | setFullName(firstName + ' ' + lastName);
37-
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
39+
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
3840
9 | }, [firstName, lastName]);
3941
10 |
4042
11 | return <div>{fullName}</div>;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ export const FIXTURE_ENTRYPOINT = {
3636
```
3737
Found 1 error:
3838
39-
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
39+
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
40+
41+
This effect updates state based on other state values. Consider calculating this value directly during render.
4042
4143
error.invalid-derived-state-from-state-in-effect.ts:10:4
4244
8 |
4345
9 | useEffect(() => {
4446
> 10 | setFullName(firstName + ' ' + lastName);
45-
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
47+
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
4648
11 | }, [firstName, lastName]);
4749
12 |
4850
13 | return (

0 commit comments

Comments
 (0)