Skip to content

Commit 3b5087b

Browse files
committed
[compiler] Fix false negatives and add data flow tree to compiler error for no-deriving-state-in-effects
Summary: Revamped the derivationCache graph. This fixes a bunch of bugs where sometimes we fail to track from which props/state we derived values from. Also, it is more intuitive and allows us to easily implement a Data Flow Tree. We can print this tree which gives insight on how the data is derived and should facilitate error resolution in complicated components Test Plan: Added a test case where we were failing to track derivations. Also updated the test cases with the new error containing the data flow tree
1 parent 663ddab commit 3b5087b

12 files changed

+281
-52
lines changed

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

Lines changed: 129 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,24 @@ class DerivationCache {
102102
typeOfValue: typeOfValue ?? 'ignored',
103103
};
104104

105-
if (sourcesIds !== undefined) {
106-
for (const id of sourcesIds) {
107-
const sourcePlace = this.cache.get(id)?.place;
105+
if (isNamedIdentifier(derivedVar)) {
106+
newValue.sourcesIds.add(derivedVar.identifier.id);
107+
}
108108

109-
if (sourcePlace === undefined) {
110-
continue;
111-
}
109+
for (const id of sourcesIds) {
110+
const sourceMetadata = this.cache.get(id);
112111

113-
/*
114-
* If the identifier of the source is a promoted identifier, then
115-
* we should set the target as the source.
116-
*/
117-
if (
118-
sourcePlace.identifier.name === null ||
119-
sourcePlace.identifier.name?.kind === 'promoted'
120-
) {
121-
newValue.sourcesIds.add(derivedVar.identifier.id);
122-
} else {
123-
newValue.sourcesIds.add(sourcePlace.identifier.id);
124-
}
112+
if (sourceMetadata === undefined) {
113+
continue;
125114
}
126-
}
127115

128-
if (newValue.sourcesIds.size === 0) {
129-
newValue.sourcesIds.add(derivedVar.identifier.id);
116+
if (isNamedIdentifier(sourceMetadata.place)) {
117+
newValue.sourcesIds.add(sourceMetadata.place.identifier.id);
118+
} else {
119+
for (const sourcesSourceId of sourceMetadata.sourcesIds) {
120+
newValue.sourcesIds.add(sourcesSourceId);
121+
}
122+
}
130123
}
131124

132125
this.cache.set(derivedVar.identifier.id, newValue);
@@ -151,6 +144,12 @@ class DerivationCache {
151144
}
152145
}
153146

147+
function isNamedIdentifier(place: Place): boolean {
148+
return (
149+
place.identifier.name !== null && place.identifier.name?.kind === 'named'
150+
);
151+
}
152+
154153
/**
155154
* Validates that useEffect is not used for derived computations which could/should
156155
* be performed in render.
@@ -202,7 +201,9 @@ export function validateNoDerivedComputationsInEffects_exp(
202201
if (param.kind === 'Identifier') {
203202
context.derivationCache.cache.set(param.identifier.id, {
204203
place: param,
205-
sourcesIds: new Set([param.identifier.id]),
204+
sourcesIds: new Set(
205+
isNamedIdentifier(param) ? [param.identifier.id] : [],
206+
),
206207
typeOfValue: 'fromProps',
207208
});
208209
}
@@ -212,7 +213,9 @@ export function validateNoDerivedComputationsInEffects_exp(
212213
if (props != null && props.kind === 'Identifier') {
213214
context.derivationCache.cache.set(props.identifier.id, {
214215
place: props,
215-
sourcesIds: new Set([props.identifier.id]),
216+
sourcesIds: new Set(
217+
isNamedIdentifier(props) ? [props.identifier.id] : [],
218+
),
216219
typeOfValue: 'fromProps',
217220
});
218221
}
@@ -293,6 +296,7 @@ function recordInstructionDerivations(
293296
if (value.kind === 'FunctionExpression') {
294297
context.functions.set(lvalue.identifier.id, value);
295298
for (const [, block] of value.loweredFunc.func.body.blocks) {
299+
recordPhiDerivations(block, context);
296300
for (const instr of block.instructions) {
297301
recordInstructionDerivations(instr, context, isFirstPass);
298302
}
@@ -341,9 +345,7 @@ function recordInstructionDerivations(
341345
}
342346

343347
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
344-
for (const id of operandMetadata.sourcesIds) {
345-
sources.add(id);
346-
}
348+
sources.add(operand.identifier.id);
347349
}
348350

349351
if (typeOfValue === 'ignored') {
@@ -411,6 +413,68 @@ function recordInstructionDerivations(
411413
}
412414
}
413415

416+
function buildDataFlowTree(
417+
sourceId: IdentifierId,
418+
indent: string = '',
419+
isLast: boolean = true,
420+
context: ValidationContext,
421+
propsSet: Set<string>,
422+
stateSet: Set<string>,
423+
): string {
424+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
425+
if (!sourceMetadata || !sourceMetadata.place.identifier.name?.value) {
426+
return '';
427+
}
428+
429+
const sourceName = sourceMetadata.place.identifier.name.value;
430+
const prefix = indent + (isLast ? '└── ' : '├── ');
431+
const childIndent = indent + (isLast ? ' ' : '│ ');
432+
433+
const childSourceIds = Array.from(sourceMetadata.sourcesIds).filter(
434+
id => id !== sourceId,
435+
);
436+
437+
const isOriginal = childSourceIds.length === 0;
438+
439+
let result = `${prefix}${sourceName}`;
440+
441+
if (isOriginal) {
442+
let typeLabel: string;
443+
if (sourceMetadata.typeOfValue === 'fromProps') {
444+
propsSet.add(sourceMetadata.place.identifier.name?.value);
445+
typeLabel = 'Prop';
446+
} else if (sourceMetadata.typeOfValue === 'fromState') {
447+
stateSet.add(sourceMetadata.place.identifier.name?.value);
448+
typeLabel = 'State';
449+
} else {
450+
propsSet.add(sourceMetadata.place.identifier.name?.value);
451+
stateSet.add(sourceMetadata.place.identifier.name?.value);
452+
typeLabel = 'Prop and State';
453+
}
454+
result += ` (${typeLabel})`;
455+
}
456+
457+
if (childSourceIds.length > 0) {
458+
result += '\n';
459+
childSourceIds.forEach((childId, index) => {
460+
const childTree = buildDataFlowTree(
461+
childId,
462+
childIndent,
463+
index === childSourceIds.length - 1,
464+
context,
465+
propsSet,
466+
stateSet,
467+
);
468+
if (childTree) {
469+
result += childTree + '\n';
470+
}
471+
});
472+
result = result.slice(0, -1);
473+
}
474+
475+
return result;
476+
}
477+
414478
function validateEffect(
415479
effectFunction: HIRFunction,
416480
context: ValidationContext,
@@ -513,27 +577,49 @@ function validateEffect(
513577
.length -
514578
1
515579
) {
516-
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
517-
.map(sourceId => {
518-
const sourceMetadata = context.derivationCache.cache.get(sourceId);
519-
return sourceMetadata?.place.identifier.name?.value;
520-
})
521-
.filter(Boolean)
522-
.join(', ');
523-
524-
let description;
525-
526-
if (derivedSetStateCall.typeOfValue === 'fromProps') {
527-
description = `From props: [${derivedDepsStr}]`;
528-
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
529-
description = `From local state: [${derivedDepsStr}]`;
530-
} else {
531-
description = `From props and local state: [${derivedDepsStr}]`;
580+
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
581+
const propsSet = new Set<string>();
582+
const stateSet = new Set<string>();
583+
584+
const trees = allSourceIds
585+
.map((id, index) =>
586+
buildDataFlowTree(
587+
id,
588+
'',
589+
index === allSourceIds.length - 1,
590+
context,
591+
propsSet,
592+
stateSet,
593+
),
594+
)
595+
.filter(Boolean);
596+
597+
const propsArr = Array.from(propsSet);
598+
const stateArr = Array.from(stateSet);
599+
600+
let rootSources = '';
601+
if (propsArr.length > 0) {
602+
rootSources += `Props: [${propsArr.join(', ')}]`;
532603
}
604+
if (stateArr.length > 0) {
605+
if (rootSources) rootSources += '\n';
606+
rootSources += `State: [${stateArr.join(', ')}]`;
607+
}
608+
609+
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
610+
611+
This setState call is setting a derived value that depends on the following reactive sources:
612+
613+
${rootSources}
614+
615+
Data Flow Tree:
616+
${trees.join('\n')}
617+
618+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
533619

534620
context.errors.pushDiagnostic(
535621
CompilerDiagnostic.create({
536-
description: `Derived values (${description}) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
622+
description: description,
537623
category: ErrorCategory.EffectDerivationsOfState,
538624
reason:
539625
'You might not need an effect. Derive values in render, not effects.',

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-conditionally-in-effect.expect.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ Found 1 error:
3434
3535
Error: You might not need an effect. Derive values in render, not effects.
3636
37-
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
37+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
38+
39+
This setState call is setting a derived value that depends on the following reactive sources:
40+
41+
Props: [value]
42+
43+
Data Flow Tree:
44+
└── value (Prop)
45+
46+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
3847
3948
error.derived-state-conditionally-in-effect.ts:9:6
4049
7 | useEffect(() => {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-default-props.expect.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,16 @@ Found 1 error:
3131
3232
Error: You might not need an effect. Derive values in render, not effects.
3333
34-
Derived values (From props: [input]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
34+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
35+
36+
This setState call is setting a derived value that depends on the following reactive sources:
37+
38+
Props: [input]
39+
40+
Data Flow Tree:
41+
└── input (Prop)
42+
43+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
3544
3645
error.derived-state-from-default-props.ts:9:4
3746
7 |

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ Found 1 error:
2828
2929
Error: You might not need an effect. Derive values in render, not effects.
3030
31-
Derived values (From local state: [count]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
31+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
32+
33+
This setState call is setting a derived value that depends on the following reactive sources:
34+
35+
State: [count]
36+
37+
Data Flow Tree:
38+
└── count (State)
39+
40+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
3241
3342
error.derived-state-from-local-state-in-effect.ts:10:6
3443
8 | useEffect(() => {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-local-state-and-component-scope.expect.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ Found 1 error:
3838
3939
Error: You might not need an effect. Derive values in render, not effects.
4040
41-
Derived values (From props and local state: [firstName, lastName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
41+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
42+
43+
This setState call is setting a derived value that depends on the following reactive sources:
44+
45+
Props: [firstName]
46+
State: [lastName]
47+
48+
Data Flow Tree:
49+
├── firstName (Prop)
50+
└── lastName (State)
51+
52+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
4253
4354
error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
4455
9 |
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoDerivedComputationsInEffects_exp
6+
7+
function Component({value}) {
8+
const [checked, setChecked] = useState('');
9+
10+
useEffect(() => {
11+
setChecked(value === '' ? [] : value.split(','));
12+
}, [value]);
13+
14+
return <div>{checked}</div>;
15+
}
16+
17+
```
18+
19+
20+
## Error
21+
22+
```
23+
Found 1 error:
24+
25+
Error: You might not need an effect. Derive values in render, not effects.
26+
27+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
28+
29+
This setState call is setting a derived value that depends on the following reactive sources:
30+
31+
Props: [value]
32+
33+
Data Flow Tree:
34+
└── value (Prop)
35+
36+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
37+
38+
error.derived-state-from-prop-setter-ternary.ts:7:4
39+
5 |
40+
6 | useEffect(() => {
41+
> 7 | setChecked(value === '' ? [] : value.split(','));
42+
| ^^^^^^^^^^ This should be computed during render, not in an effect
43+
8 | }, [value]);
44+
9 |
45+
10 | return <div>{checked}</div>;
46+
```
47+
48+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @validateNoDerivedComputationsInEffects_exp
2+
3+
function Component({value}) {
4+
const [checked, setChecked] = useState('');
5+
6+
useEffect(() => {
7+
setChecked(value === '' ? [] : value.split(','));
8+
}, [value]);
9+
10+
return <div>{checked}</div>;
11+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-with-side-effect.expect.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,16 @@ Found 1 error:
3131
3232
Error: You might not need an effect. Derive values in render, not effects.
3333
34-
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
34+
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
35+
36+
This setState call is setting a derived value that depends on the following reactive sources:
37+
38+
Props: [value]
39+
40+
Data Flow Tree:
41+
└── value (Prop)
42+
43+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
3544
3645
error.derived-state-from-prop-with-side-effect.ts:8:4
3746
6 |

0 commit comments

Comments
 (0)