@samitouri / QOS-React-2 / commits / 6347c6d373

[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

Jorge Cabiedes committed Nov 10, 2025 at 12:09 UTC 6347c6d37336c7791098d2d817b22f02ea41a5d3
12 files changed +350 -64
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts
+198 -55
@@ -33,6 +33,7 @@ type DerivationMetadata = {
33 typeOfValue: TypeOfValue;
34 place: Place;
35 sourcesIds: Set<IdentifierId>;
36 + isStateSource: boolean;
37 };
38
39 type ValidationContext = {
@@ -56,6 +57,7 @@ class DerivationCache {
57 place: value.place,
58 sourcesIds: new Set(value.sourcesIds),
59 typeOfValue: value.typeOfValue,
60 + isStateSource: value.isStateSource,
61 });
62 }
63 }
@@ -95,41 +97,28 @@ class DerivationCache {
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(
@@ -151,6 +140,14 @@ class DerivationCache {
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.
@@ -202,8 +199,9 @@ export function validateNoDerivedComputationsInEffects_exp(
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 }
@@ -212,8 +210,9 @@ export function validateNoDerivedComputationsInEffects_exp(
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 }
@@ -267,6 +266,7 @@ function recordPhiDerivations(
266 phi.place,
267 sourcesIds,
268 typeOfValue,
269 + false,
270 );
271 }
272 }
@@ -288,11 +288,13 @@ function recordInstructionDerivations(
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 }
@@ -311,10 +313,7 @@ function recordInstructionDerivations(
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 }
@@ -341,9 +340,7 @@ function recordInstructionDerivations(
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') {
@@ -351,7 +348,12 @@ function recordInstructionDerivations(
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)) {
@@ -378,6 +380,7 @@ function recordInstructionDerivations(
380 operand,
381 sources,
382 typeOfValue,
383 + false,
384 );
385 }
386 }
@@ -411,6 +414,117 @@ function recordInstructionDerivations(
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,
@@ -513,27 +627,56 @@ function validateEffect(
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.',
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-conditionally-in-effect.expect.md
+10 -1
@@ -34,7 +34,16 @@ Found 1 error:
34
35 Error: You might not need an effect. Derive values in render, not effects.
36
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.
47
48 error.derived-state-conditionally-in-effect.ts:9:6
49 7 | useEffect(() => {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-default-props.expect.md
+10 -1
@@ -31,7 +31,16 @@ Found 1 error:
31
32 Error: You might not need an effect. Derive values in render, not effects.
33
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.
44
45 error.derived-state-from-default-props.ts:9:4
46 7 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-local-state-in-effect.expect.md
+10 -1
@@ -28,7 +28,16 @@ Found 1 error:
28
29 Error: You might not need an effect. Derive values in render, not effects.
30
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.
41
42 error.derived-state-from-local-state-in-effect.ts:10:6
43 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
+12 -1
@@ -38,7 +38,18 @@ Found 1 error:
38
39 Error: You might not need an effect. Derive values in render, not effects.
40
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.
53
54 error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
55 9 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-setter-ternary.expect.md new
+48
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-setter-ternary.js new
+11
@@ -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
+10 -1
@@ -31,7 +31,16 @@ Found 1 error:
31
32 Error: You might not need an effect. Derive values in render, not effects.
33
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.
44
45 error.derived-state-from-prop-with-side-effect.ts:8:4
46 6 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.effect-contains-local-function-call.expect.md
+10 -1
@@ -35,7 +35,16 @@ Found 1 error:
35
36 Error: You might not need an effect. Derive values in render, not effects.
37
38 -Derived values (From props: [propValue]) 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.
38 +Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
39 +
40 +This setState call is setting a derived value that depends on the following reactive sources:
41 +
42 +Props: [propValue]
43 +
44 +Data Flow Tree:
45 +└── propValue (Prop)
46 +
47 +See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
48
49 error.effect-contains-local-function-call.ts:12:4
50 10 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-computation-in-effect.expect.md
+10 -1
@@ -33,7 +33,16 @@ Found 1 error:
33
34 Error: You might not need an effect. Derive values in render, not effects.
35
36 -Derived values (From local state: [firstName]) 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.
36 +Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
37 +
38 +This setState call is setting a derived value that depends on the following reactive sources:
39 +
40 +State: [firstName]
41 +
42 +Data Flow Tree:
43 +└── firstName (State)
44 +
45 +See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
46
47 error.invalid-derived-computation-in-effect.ts:11:4
48 9 | const [fullName, setFullName] = useState('');
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-computed-props.expect.md
+11 -1
@@ -31,7 +31,17 @@ Found 1 error:
31
32 Error: You might not need an effect. Derive values in render, not effects.
33
34 -Derived values (From props: [props]) 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: [props]
39 +
40 +Data Flow Tree:
41 +└── computed
42 + └── props (Prop)
43 +
44 +See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
45
46 error.invalid-derived-state-from-computed-props.ts:9:4
47 7 | useEffect(() => {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-destructured-props.expect.md
+10 -1
@@ -32,7 +32,16 @@ Found 1 error:
32
33 Error: You might not need an effect. Derive values in render, not effects.
34
35 -Derived values (From props: [props]) 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.
35 +Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
36 +
37 +This setState call is setting a derived value that depends on the following reactive sources:
38 +
39 +Props: [props]
40 +
41 +Data Flow Tree:
42 +└── props (Prop)
43 +
44 +See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
45
46 error.invalid-derived-state-from-destructured-props.ts:10:4
47 8 |