Skip to content

Commit 6347c6d

Browse files
authored
[compiler] Fix false negatives and add data flow tree to compiler error for no-deriving-state-in-effects (#34995)
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 --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34995). * #35044 * #35020 * #34973 * #34972 * __->__ #34995 * #34967
1 parent 01fb328 commit 6347c6d

12 files changed

+350
-64
lines changed

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

Lines changed: 198 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type DerivationMetadata = {
3333
typeOfValue: TypeOfValue;
3434
place: Place;
3535
sourcesIds: Set<IdentifierId>;
36+
isStateSource: boolean;
3637
};
3738

3839
type ValidationContext = {
@@ -56,6 +57,7 @@ class DerivationCache {
5657
place: value.place,
5758
sourcesIds: new Set(value.sourcesIds),
5859
typeOfValue: value.typeOfValue,
60+
isStateSource: value.isStateSource,
5961
});
6062
}
6163
}
@@ -95,41 +97,28 @@ class DerivationCache {
9597
derivedVar: Place,
9698
sourcesIds: Set<IdentifierId>,
9799
typeOfValue: TypeOfValue,
100+
isStateSource: boolean,
98101
): void {
99-
let newValue: DerivationMetadata = {
100-
place: derivedVar,
101-
sourcesIds: new Set(),
102-
typeOfValue: typeOfValue ?? 'ignored',
103-
};
104-
105-
if (sourcesIds !== undefined) {
106-
for (const id of sourcesIds) {
107-
const sourcePlace = this.cache.get(id)?.place;
108-
109-
if (sourcePlace === undefined) {
110-
continue;
111-
}
112-
113-
/*
114-
* If the identifier of the source is a promoted identifier, then
115-
* we should set the target as the source.
116-
*/
102+
let finalIsSource = isStateSource;
103+
if (!finalIsSource) {
104+
for (const sourceId of sourcesIds) {
105+
const sourceMetadata = this.cache.get(sourceId);
117106
if (
118-
sourcePlace.identifier.name === null ||
119-
sourcePlace.identifier.name?.kind === 'promoted'
107+
sourceMetadata?.isStateSource &&
108+
sourceMetadata.place.identifier.name?.kind !== 'named'
120109
) {
121-
newValue.sourcesIds.add(derivedVar.identifier.id);
122-
} else {
123-
newValue.sourcesIds.add(sourcePlace.identifier.id);
110+
finalIsSource = true;
111+
break;
124112
}
125113
}
126114
}
127115

128-
if (newValue.sourcesIds.size === 0) {
129-
newValue.sourcesIds.add(derivedVar.identifier.id);
130-
}
131-
132-
this.cache.set(derivedVar.identifier.id, newValue);
116+
this.cache.set(derivedVar.identifier.id, {
117+
place: derivedVar,
118+
sourcesIds: sourcesIds,
119+
typeOfValue: typeOfValue ?? 'ignored',
120+
isStateSource: finalIsSource,
121+
});
133122
}
134123

135124
private isDerivationEqual(
@@ -151,6 +140,14 @@ class DerivationCache {
151140
}
152141
}
153142

143+
function isNamedIdentifier(place: Place): place is Place & {
144+
identifier: {name: NonNullable<Place['identifier']['name']>};
145+
} {
146+
return (
147+
place.identifier.name !== null && place.identifier.name.kind === 'named'
148+
);
149+
}
150+
154151
/**
155152
* Validates that useEffect is not used for derived computations which could/should
156153
* be performed in render.
@@ -202,8 +199,9 @@ export function validateNoDerivedComputationsInEffects_exp(
202199
if (param.kind === 'Identifier') {
203200
context.derivationCache.cache.set(param.identifier.id, {
204201
place: param,
205-
sourcesIds: new Set([param.identifier.id]),
202+
sourcesIds: new Set(),
206203
typeOfValue: 'fromProps',
204+
isStateSource: true,
207205
});
208206
}
209207
}
@@ -212,8 +210,9 @@ export function validateNoDerivedComputationsInEffects_exp(
212210
if (props != null && props.kind === 'Identifier') {
213211
context.derivationCache.cache.set(props.identifier.id, {
214212
place: props,
215-
sourcesIds: new Set([props.identifier.id]),
213+
sourcesIds: new Set(),
216214
typeOfValue: 'fromProps',
215+
isStateSource: true,
217216
});
218217
}
219218
}
@@ -267,6 +266,7 @@ function recordPhiDerivations(
267266
phi.place,
268267
sourcesIds,
269268
typeOfValue,
269+
false,
270270
);
271271
}
272272
}
@@ -288,11 +288,13 @@ function recordInstructionDerivations(
288288
isFirstPass: boolean,
289289
): void {
290290
let typeOfValue: TypeOfValue = 'ignored';
291+
let isSource: boolean = false;
291292
const sources: Set<IdentifierId> = new Set();
292293
const {lvalue, value} = instr;
293294
if (value.kind === 'FunctionExpression') {
294295
context.functions.set(lvalue.identifier.id, value);
295296
for (const [, block] of value.loweredFunc.func.body.blocks) {
297+
recordPhiDerivations(block, context);
296298
for (const instr of block.instructions) {
297299
recordInstructionDerivations(instr, context, isFirstPass);
298300
}
@@ -311,10 +313,7 @@ function recordInstructionDerivations(
311313
context.effects.add(effectFunction.loweredFunc.func);
312314
}
313315
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
314-
const stateValueSource = value.args[0];
315-
if (stateValueSource.kind === 'Identifier') {
316-
sources.add(stateValueSource.identifier.id);
317-
}
316+
isSource = true;
318317
typeOfValue = joinValue(typeOfValue, 'fromState');
319318
}
320319
}
@@ -341,17 +340,20 @@ function recordInstructionDerivations(
341340
}
342341

343342
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
344-
for (const id of operandMetadata.sourcesIds) {
345-
sources.add(id);
346-
}
343+
sources.add(operand.identifier.id);
347344
}
348345

349346
if (typeOfValue === 'ignored') {
350347
return;
351348
}
352349

353350
for (const lvalue of eachInstructionLValue(instr)) {
354-
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
351+
context.derivationCache.addDerivationEntry(
352+
lvalue,
353+
sources,
354+
typeOfValue,
355+
isSource,
356+
);
355357
}
356358

357359
for (const operand of eachInstructionOperand(instr)) {
@@ -378,6 +380,7 @@ function recordInstructionDerivations(
378380
operand,
379381
sources,
380382
typeOfValue,
383+
false,
381384
);
382385
}
383386
}
@@ -411,6 +414,117 @@ function recordInstructionDerivations(
411414
}
412415
}
413416

417+
type TreeNode = {
418+
name: string;
419+
typeOfValue: TypeOfValue;
420+
isSource: boolean;
421+
children: Array<TreeNode>;
422+
};
423+
424+
function buildTreeNode(
425+
sourceId: IdentifierId,
426+
context: ValidationContext,
427+
visited: Set<string> = new Set(),
428+
): Array<TreeNode> {
429+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
430+
if (!sourceMetadata) {
431+
return [];
432+
}
433+
434+
if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
435+
return [
436+
{
437+
name: sourceMetadata.place.identifier.name.value,
438+
typeOfValue: sourceMetadata.typeOfValue,
439+
isSource: sourceMetadata.isStateSource,
440+
children: [],
441+
},
442+
];
443+
}
444+
445+
const children: Array<TreeNode> = [];
446+
447+
const namedSiblings: Set<string> = new Set();
448+
for (const childId of sourceMetadata.sourcesIds) {
449+
const childNodes = buildTreeNode(
450+
childId,
451+
context,
452+
new Set([
453+
...visited,
454+
...(isNamedIdentifier(sourceMetadata.place)
455+
? [sourceMetadata.place.identifier.name.value]
456+
: []),
457+
]),
458+
);
459+
if (childNodes) {
460+
for (const childNode of childNodes) {
461+
if (!namedSiblings.has(childNode.name)) {
462+
children.push(childNode);
463+
namedSiblings.add(childNode.name);
464+
}
465+
}
466+
}
467+
}
468+
469+
if (
470+
isNamedIdentifier(sourceMetadata.place) &&
471+
!visited.has(sourceMetadata.place.identifier.name.value)
472+
) {
473+
return [
474+
{
475+
name: sourceMetadata.place.identifier.name.value,
476+
typeOfValue: sourceMetadata.typeOfValue,
477+
isSource: sourceMetadata.isStateSource,
478+
children: children,
479+
},
480+
];
481+
}
482+
483+
return children;
484+
}
485+
486+
function renderTree(
487+
node: TreeNode,
488+
indent: string = '',
489+
isLast: boolean = true,
490+
propsSet: Set<string>,
491+
stateSet: Set<string>,
492+
): string {
493+
const prefix = indent + (isLast ? '└── ' : '├── ');
494+
const childIndent = indent + (isLast ? ' ' : '│ ');
495+
496+
let result = `${prefix}${node.name}`;
497+
498+
if (node.isSource) {
499+
let typeLabel: string;
500+
if (node.typeOfValue === 'fromProps') {
501+
propsSet.add(node.name);
502+
typeLabel = 'Prop';
503+
} else if (node.typeOfValue === 'fromState') {
504+
stateSet.add(node.name);
505+
typeLabel = 'State';
506+
} else {
507+
propsSet.add(node.name);
508+
stateSet.add(node.name);
509+
typeLabel = 'Prop and State';
510+
}
511+
result += ` (${typeLabel})`;
512+
}
513+
514+
if (node.children.length > 0) {
515+
result += '\n';
516+
node.children.forEach((child, index) => {
517+
const isLastChild = index === node.children.length - 1;
518+
result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
519+
if (index < node.children.length - 1) {
520+
result += '\n';
521+
}
522+
});
523+
}
524+
525+
return result;
526+
}
527+
414528
function validateEffect(
415529
effectFunction: HIRFunction,
416530
context: ValidationContext,
@@ -513,27 +627,56 @@ function validateEffect(
513627
.length -
514628
1
515629
) {
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}]`;
630+
const propsSet = new Set<string>();
631+
const stateSet = new Set<string>();
632+
633+
const rootNodesMap = new Map<string, TreeNode>();
634+
for (const id of derivedSetStateCall.sourceIds) {
635+
const nodes = buildTreeNode(id, context);
636+
for (const node of nodes) {
637+
if (!rootNodesMap.has(node.name)) {
638+
rootNodesMap.set(node.name, node);
639+
}
640+
}
641+
}
642+
const rootNodes = Array.from(rootNodesMap.values());
643+
644+
const trees = rootNodes.map((node, index) =>
645+
renderTree(
646+
node,
647+
'',
648+
index === rootNodes.length - 1,
649+
propsSet,
650+
stateSet,
651+
),
652+
);
653+
654+
const propsArr = Array.from(propsSet);
655+
const stateArr = Array.from(stateSet);
656+
657+
let rootSources = '';
658+
if (propsArr.length > 0) {
659+
rootSources += `Props: [${propsArr.join(', ')}]`;
532660
}
661+
if (stateArr.length > 0) {
662+
if (rootSources) rootSources += '\n';
663+
rootSources += `State: [${stateArr.join(', ')}]`;
664+
}
665+
666+
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
667+
668+
This setState call is setting a derived value that depends on the following reactive sources:
669+
670+
${rootSources}
671+
672+
Data Flow Tree:
673+
${trees.join('\n')}
674+
675+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
533676

534677
context.errors.pushDiagnostic(
535678
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`,
679+
description: description,
537680
category: ErrorCategory.EffectDerivationsOfState,
538681
reason:
539682
'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(() => {

0 commit comments

Comments
 (0)