33
typeOfValue: TypeOfValue;
34
place: Place;
35
sourcesIds: Set<IdentifierId>;
36
+ isStateSource: boolean;
37
};
38
39
type ValidationContext = {
57
place: value.place,
58
sourcesIds: new Set(value.sourcesIds),
59
typeOfValue: value.typeOfValue,
60
+ isStateSource: value.isStateSource,
61
});
62
}
63
}
97
derivedVar: Place,
98
sourcesIds: Set<IdentifierId>,
99
typeOfValue: TypeOfValue,
100
+ isStateSource: boolean,
101
): 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);
106
if (
118
- sourcePlace.identifier.name === null ||
119
- sourcePlace.identifier.name?.kind === 'promoted'
107
+ sourceMetadata?.isStateSource &&
108
+ sourceMetadata.place.identifier.name?.kind !== 'named'
109
) {
121
- newValue.sourcesIds.add(derivedVar.identifier.id);
122
- } else {
123
- newValue.sourcesIds.add(sourcePlace.identifier.id);
110
+ finalIsSource = true;
111
+ break;
112
}
113
}
114
}
115
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
+ });
122
}
123
124
private isDerivationEqual(
140
}
141
}
142
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
+
151
/**
152
* Validates that useEffect is not used for derived computations which could/should
153
* be performed in render.
199
if (param.kind === 'Identifier') {
200
context.derivationCache.cache.set(param.identifier.id, {
201
place: param,
205
- sourcesIds: new Set([param.identifier.id]),
202
+ sourcesIds: new Set(),
203
typeOfValue: 'fromProps',
204
+ isStateSource: true,
205
});
206
}
207
}
210
if (props != null && props.kind === 'Identifier') {
211
context.derivationCache.cache.set(props.identifier.id, {
212
place: props,
215
- sourcesIds: new Set([props.identifier.id]),
213
+ sourcesIds: new Set(),
214
typeOfValue: 'fromProps',
215
+ isStateSource: true,
216
});
217
}
218
}
266
phi.place,
267
sourcesIds,
268
typeOfValue,
269
+ false,
270
);
271
}
272
}
288
isFirstPass: boolean,
289
): void {
290
let typeOfValue: TypeOfValue = 'ignored';
291
+ let isSource: boolean = false;
292
const sources: Set<IdentifierId> = new Set();
293
const {lvalue, value} = instr;
294
if (value.kind === 'FunctionExpression') {
295
context.functions.set(lvalue.identifier.id, value);
296
for (const [, block] of value.loweredFunc.func.body.blocks) {
297
+ recordPhiDerivations(block, context);
298
for (const instr of block.instructions) {
299
recordInstructionDerivations(instr, context, isFirstPass);
300
}
313
context.effects.add(effectFunction.loweredFunc.func);
314
}
315
} 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;
317
typeOfValue = joinValue(typeOfValue, 'fromState');
318
}
319
}
340
}
341
342
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
344
- for (const id of operandMetadata.sourcesIds) {
345
- sources.add(id);
346
- }
343
+ sources.add(operand.identifier.id);
344
}
345
346
if (typeOfValue === 'ignored') {
348
}
349
350
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
+ );
357
}
358
359
for (const operand of eachInstructionOperand(instr)) {
380
operand,
381
sources,
382
typeOfValue,
383
+ false,
384
);
385
}
386
}
414
}
415
}
416
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
+
528
function validateEffect(
529
effectFunction: HIRFunction,
530
context: ValidationContext,
627
.length -
628
1
629
) {
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(', ')}]`;
660
}
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`;
676
677
context.errors.pushDiagnostic(
678
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,
680
category: ErrorCategory.EffectDerivationsOfState,
681
reason:
682
'You might not need an effect. Derive values in render, not effects.',