Skip to content

Commit 4af24bf

Browse files
committed
[compiler] Add data flow tree to compiler error for no-deriving-state-in-effects
1 parent a54e03c commit 4af24bf

File tree

1 file changed

+137
-44
lines changed

1 file changed

+137
-44
lines changed

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

Lines changed: 137 additions & 44 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
}
@@ -223,10 +226,10 @@ export function validateNoDerivedComputationsInEffects_exp(
223226
context.derivationCache.takeSnapshot();
224227

225228
for (const block of fn.body.blocks.values()) {
229+
recordPhiDerivations(block, context);
226230
for (const instr of block.instructions) {
227231
recordInstructionDerivations(instr, context, isFirstPass);
228232
}
229-
recordPhiDerivations(block, context);
230233
}
231234

232235
context.derivationCache.checkForChanges();
@@ -237,6 +240,7 @@ export function validateNoDerivedComputationsInEffects_exp(
237240
validateEffect(effect, context);
238241
}
239242

243+
console.log(derivationCache.cache);
240244
if (errors.hasAnyErrors()) {
241245
throw errors;
242246
}
@@ -293,6 +297,7 @@ function recordInstructionDerivations(
293297
if (value.kind === 'FunctionExpression') {
294298
context.functions.set(lvalue.identifier.id, value);
295299
for (const [, block] of value.loweredFunc.func.body.blocks) {
300+
recordPhiDerivations(block, context);
296301
for (const instr of block.instructions) {
297302
recordInstructionDerivations(instr, context, isFirstPass);
298303
}
@@ -341,9 +346,7 @@ function recordInstructionDerivations(
341346
}
342347

343348
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
344-
for (const id of operandMetadata.sourcesIds) {
345-
sources.add(id);
346-
}
349+
sources.add(operand.identifier.id);
347350
}
348351

349352
if (typeOfValue === 'ignored') {
@@ -406,6 +409,60 @@ function recordInstructionDerivations(
406409
}
407410
}
408411

412+
function buildDataFlowTree(
413+
sourceId: IdentifierId,
414+
indent: string = '',
415+
isLast: boolean = true,
416+
context: ValidationContext,
417+
): string {
418+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
419+
if (!sourceMetadata || !sourceMetadata.place.identifier.name?.value) {
420+
return '';
421+
}
422+
423+
const sourceName = sourceMetadata.place.identifier.name.value;
424+
const prefix = indent + (isLast ? '└── ' : '├── ');
425+
const childIndent = indent + (isLast ? ' ' : '│ ');
426+
427+
const childSourceIds = Array.from(sourceMetadata.sourcesIds).filter(
428+
id => id !== sourceId,
429+
);
430+
431+
const isOriginal = childSourceIds.length === 0;
432+
433+
let result = `${prefix}${sourceName}`;
434+
435+
if (isOriginal) {
436+
let typeLabel: string;
437+
if (sourceMetadata.typeOfValue === 'fromProps') {
438+
typeLabel = 'Prop';
439+
} else if (sourceMetadata.typeOfValue === 'fromState') {
440+
typeLabel = 'State';
441+
} else {
442+
typeLabel = 'Prop and State';
443+
}
444+
result += ` (${typeLabel})`;
445+
}
446+
447+
if (childSourceIds.length > 0) {
448+
result += '\n';
449+
childSourceIds.forEach((childId, index) => {
450+
const childTree = buildDataFlowTree(
451+
childId,
452+
childIndent,
453+
index === childSourceIds.length - 1,
454+
context,
455+
);
456+
if (childTree) {
457+
result += childTree + '\n';
458+
}
459+
});
460+
result = result.slice(0, -1);
461+
}
462+
463+
return result;
464+
}
465+
409466
function validateEffect(
410467
effectFunction: HIRFunction,
411468
context: ValidationContext,
@@ -508,27 +565,63 @@ function validateEffect(
508565
.length -
509566
1
510567
) {
511-
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
512-
.map(sourceId => {
513-
const sourceMetadata = context.derivationCache.cache.get(sourceId);
514-
return sourceMetadata?.place.identifier.name?.value;
515-
})
516-
.filter(Boolean)
517-
.join(', ');
518-
519-
let description;
520-
521-
if (derivedSetStateCall.typeOfValue === 'fromProps') {
522-
description = `From props: [${derivedDepsStr}]`;
523-
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
524-
description = `From local state: [${derivedDepsStr}]`;
525-
} else {
526-
description = `From props and local state: [${derivedDepsStr}]`;
568+
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
569+
const trees = allSourceIds
570+
.map((id, index) =>
571+
buildDataFlowTree(id, '', index === allSourceIds.length - 1, context),
572+
)
573+
.filter(Boolean);
574+
575+
const propsSet = new Set<string>();
576+
const stateSet = new Set<string>();
577+
578+
for (const sourceId of derivedSetStateCall.sourceIds) {
579+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
580+
if (
581+
sourceMetadata &&
582+
sourceMetadata.place.identifier.name?.value &&
583+
(sourceMetadata.sourcesIds.size === 0 ||
584+
(sourceMetadata.sourcesIds.size === 1 &&
585+
sourceMetadata.sourcesIds.has(sourceId)))
586+
) {
587+
const name = sourceMetadata.place.identifier.name.value;
588+
if (sourceMetadata.typeOfValue === 'fromProps') {
589+
propsSet.add(name);
590+
} else if (sourceMetadata.typeOfValue === 'fromState') {
591+
stateSet.add(name);
592+
} else if (sourceMetadata.typeOfValue === 'fromPropsAndState') {
593+
propsSet.add(name);
594+
stateSet.add(name);
595+
}
596+
}
597+
}
598+
599+
const propsArr = Array.from(propsSet);
600+
const stateArr = Array.from(stateSet);
601+
602+
let rootSources = '';
603+
if (propsArr.length > 0) {
604+
rootSources += `Props: [${propsArr.join(', ')}]`;
527605
}
606+
if (stateArr.length > 0) {
607+
if (rootSources) rootSources += '\n';
608+
rootSources += `State: [${stateArr.join(', ')}]`;
609+
}
610+
611+
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
612+
613+
This setState call is setting a derived value that depends on the following reactive sources:
614+
615+
${rootSources}
616+
617+
Data Flow Tree:
618+
${trees.join('\n')}
619+
620+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
528621

529622
context.errors.pushDiagnostic(
530623
CompilerDiagnostic.create({
531-
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`,
624+
description: description,
532625
category: ErrorCategory.EffectDerivationsOfState,
533626
reason:
534627
'You might not need an effect. Derive values in render, not effects.',

0 commit comments

Comments
 (0)