Skip to content

Commit e8bbaf6

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

File tree

1 file changed

+138
-46
lines changed

1 file changed

+138
-46
lines changed

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

Lines changed: 138 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
GeneratedSource,
2424
SourceLocation,
2525
} from '../HIR';
26+
import {printInstruction} from '../HIR/PrintHIR';
2627
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
2728
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
2829
import {assertExhaustive} from '../Utils/utils';
@@ -102,31 +103,24 @@ class DerivationCache {
102103
typeOfValue: typeOfValue ?? 'ignored',
103104
};
104105

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

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

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-
}
113+
if (sourceMetadata === undefined) {
114+
continue;
125115
}
126-
}
127116

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

132126
this.cache.set(derivedVar.identifier.id, newValue);
@@ -151,6 +145,12 @@ class DerivationCache {
151145
}
152146
}
153147

148+
function isNamedIdentifier(place: Place): Boolean {
149+
return (
150+
place.identifier.name !== null && place.identifier.name?.kind === 'named'
151+
);
152+
}
153+
154154
/**
155155
* Validates that useEffect is not used for derived computations which could/should
156156
* be performed in render.
@@ -202,7 +202,9 @@ export function validateNoDerivedComputationsInEffects_exp(
202202
if (param.kind === 'Identifier') {
203203
context.derivationCache.cache.set(param.identifier.id, {
204204
place: param,
205-
sourcesIds: new Set([param.identifier.id]),
205+
sourcesIds: new Set(
206+
isNamedIdentifier(param) ? [param.identifier.id] : [],
207+
),
206208
typeOfValue: 'fromProps',
207209
});
208210
}
@@ -212,7 +214,9 @@ export function validateNoDerivedComputationsInEffects_exp(
212214
if (props != null && props.kind === 'Identifier') {
213215
context.derivationCache.cache.set(props.identifier.id, {
214216
place: props,
215-
sourcesIds: new Set([props.identifier.id]),
217+
sourcesIds: new Set(
218+
isNamedIdentifier(props) ? [props.identifier.id] : [],
219+
),
216220
typeOfValue: 'fromProps',
217221
});
218222
}
@@ -223,10 +227,10 @@ export function validateNoDerivedComputationsInEffects_exp(
223227
context.derivationCache.takeSnapshot();
224228

225229
for (const block of fn.body.blocks.values()) {
230+
recordPhiDerivations(block, context);
226231
for (const instr of block.instructions) {
227232
recordInstructionDerivations(instr, context, isFirstPass);
228233
}
229-
recordPhiDerivations(block, context);
230234
}
231235

232236
context.derivationCache.checkForChanges();
@@ -237,6 +241,7 @@ export function validateNoDerivedComputationsInEffects_exp(
237241
validateEffect(effect, context);
238242
}
239243

244+
console.log(derivationCache.cache);
240245
if (errors.hasAnyErrors()) {
241246
throw errors;
242247
}
@@ -293,6 +298,7 @@ function recordInstructionDerivations(
293298
if (value.kind === 'FunctionExpression') {
294299
context.functions.set(lvalue.identifier.id, value);
295300
for (const [, block] of value.loweredFunc.func.body.blocks) {
301+
recordPhiDerivations(block, context);
296302
for (const instr of block.instructions) {
297303
recordInstructionDerivations(instr, context, isFirstPass);
298304
}
@@ -341,9 +347,7 @@ function recordInstructionDerivations(
341347
}
342348

343349
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
344-
for (const id of operandMetadata.sourcesIds) {
345-
sources.add(id);
346-
}
350+
sources.add(operand.identifier.id);
347351
}
348352

349353
if (typeOfValue === 'ignored') {
@@ -406,6 +410,60 @@ function recordInstructionDerivations(
406410
}
407411
}
408412

413+
function buildDataFlowTree(
414+
sourceId: IdentifierId,
415+
indent: string = '',
416+
isLast: boolean = true,
417+
context: ValidationContext,
418+
): string {
419+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
420+
if (!sourceMetadata || !sourceMetadata.place.identifier.name?.value) {
421+
return '';
422+
}
423+
424+
const sourceName = sourceMetadata.place.identifier.name.value;
425+
const prefix = indent + (isLast ? '└── ' : '├── ');
426+
const childIndent = indent + (isLast ? ' ' : '│ ');
427+
428+
const childSourceIds = Array.from(sourceMetadata.sourcesIds).filter(
429+
id => id !== sourceId,
430+
);
431+
432+
const isOriginal = childSourceIds.length === 0;
433+
434+
let result = `${prefix}${sourceName}`;
435+
436+
if (isOriginal) {
437+
let typeLabel: string;
438+
if (sourceMetadata.typeOfValue === 'fromProps') {
439+
typeLabel = 'Prop';
440+
} else if (sourceMetadata.typeOfValue === 'fromState') {
441+
typeLabel = 'State';
442+
} else {
443+
typeLabel = 'Prop and State';
444+
}
445+
result += ` (${typeLabel})`;
446+
}
447+
448+
if (childSourceIds.length > 0) {
449+
result += '\n';
450+
childSourceIds.forEach((childId, index) => {
451+
const childTree = buildDataFlowTree(
452+
childId,
453+
childIndent,
454+
index === childSourceIds.length - 1,
455+
context,
456+
);
457+
if (childTree) {
458+
result += childTree + '\n';
459+
}
460+
});
461+
result = result.slice(0, -1);
462+
}
463+
464+
return result;
465+
}
466+
409467
function validateEffect(
410468
effectFunction: HIRFunction,
411469
context: ValidationContext,
@@ -508,34 +566,68 @@ function validateEffect(
508566
.length -
509567
1
510568
) {
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}]`;
569+
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
570+
const trees = allSourceIds
571+
.map((id, index) =>
572+
buildDataFlowTree(id, '', index === allSourceIds.length - 1, context),
573+
)
574+
.filter(Boolean);
575+
576+
const propsSet = new Set<string>();
577+
const stateSet = new Set<string>();
578+
579+
for (const sourceId of derivedSetStateCall.sourceIds) {
580+
const sourceMetadata = context.derivationCache.cache.get(sourceId);
581+
if (
582+
sourceMetadata &&
583+
sourceMetadata.place.identifier.name?.value &&
584+
(sourceMetadata.sourcesIds.size === 0 ||
585+
(sourceMetadata.sourcesIds.size === 1 &&
586+
sourceMetadata.sourcesIds.has(sourceId)))
587+
) {
588+
const name = sourceMetadata.place.identifier.name.value;
589+
if (sourceMetadata.typeOfValue === 'fromProps') {
590+
propsSet.add(name);
591+
} else if (sourceMetadata.typeOfValue === 'fromState') {
592+
stateSet.add(name);
593+
} else if (sourceMetadata.typeOfValue === 'fromPropsAndState') {
594+
propsSet.add(name);
595+
stateSet.add(name);
596+
}
597+
}
598+
}
599+
600+
const propsArr = Array.from(propsSet);
601+
const stateArr = Array.from(stateSet);
602+
603+
let rootSources = '';
604+
if (propsArr.length > 0) {
605+
rootSources += `Props: [${propsArr.join(', ')}]`;
527606
}
607+
if (stateArr.length > 0) {
608+
if (rootSources) rootSources += '\n';
609+
rootSources += `State: [${stateArr.join(', ')}]`;
610+
}
611+
612+
const description = `This setState call is setting a derived value that depends on the following reactive sources:
613+
614+
${rootSources}
615+
616+
Data Flow Tree:
617+
${trees.join('\n')}
618+
619+
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
528620

529621
context.errors.pushDiagnostic(
530622
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`,
623+
description: description,
532624
category: ErrorCategory.EffectDerivationsOfState,
533625
reason:
534-
'You might not need an effect. Derive values in render, not effects.',
626+
'Avoid derived computations in effects. Compute values during render instead.',
535627
}).withDetails({
536628
kind: 'error',
537629
loc: derivedSetStateCall.value.callee.loc,
538-
message: 'This should be computed during render, not in an effect',
630+
message: 'Move this computation to render',
539631
}),
540632
);
541633
}

0 commit comments

Comments
 (0)