[Compiler] Avoid capturing global setStates for no-derived-computations lint (#35135)
Summary: This only matters when enableTreatSetIdentifiersAsStateSetters=true This pattern is still bad. But Right now the validation can only recommend to move stuff to "calculate in render" A global setState should not be moved to render, not even conditionally and you can't remove state without crossing Component boundaries, which makes this a different kind of fix. So while we are only suggesting "calculate in render" as a fix we should disallow the lint from throwing in this case IMO Test Plan: Added a fixture --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35135). * __->__ #35135 * #35134
Jorge Cabiedes committed
Nov 13, 2025 at 22:56 UTC
257b033fc7b0518e7b1db32ca24e2354933b9d0e
4 files changed
+86
-1
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts
+12
@@ -690,6 +690,18 @@ function validateEffect(
690
instr.value.args.length === 1 &&
691
instr.value.args[0].kind === 'Identifier'
692
) {
693
+ const calleeMetadata = context.derivationCache.cache.get(
694
+ instr.value.callee.identifier.id,
695
+ );
696
+
697
+ /*
698
+ * If the setState comes from a source other than local state skip
699
+ * since the fix is not to calculate in render
700
+ */
701
+ if (calleeMetadata?.typeOfValue != 'fromState') {
702
+ continue;
703
+ }
704
+
705
const argMetadata = context.derivationCache.cache.get(
706
instr.value.args[0].identifier.id,
707
);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/from-props-setstate-in-effect-no-error.expect.md
new
+65
@@ -0,0 +1,65 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
6
+
7
+function Component({setParentState, prop}) {
8
+ useEffect(() => {
9
+ setParentState(prop);
10
+ }, [prop]);
11
+
12
+ return <div>{prop}</div>;
13
+}
14
+
15
+```
16
+
17
+## Code
18
+
19
+```javascript
20
+import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
21
+
22
+function Component(t0) {
23
+ const $ = _c(7);
24
+ const { setParentState, prop } = t0;
25
+ let t1;
26
+ if ($[0] !== prop || $[1] !== setParentState) {
27
+ t1 = () => {
28
+ setParentState(prop);
29
+ };
30
+ $[0] = prop;
31
+ $[1] = setParentState;
32
+ $[2] = t1;
33
+ } else {
34
+ t1 = $[2];
35
+ }
36
+ let t2;
37
+ if ($[3] !== prop) {
38
+ t2 = [prop];
39
+ $[3] = prop;
40
+ $[4] = t2;
41
+ } else {
42
+ t2 = $[4];
43
+ }
44
+ useEffect(t1, t2);
45
+ let t3;
46
+ if ($[5] !== prop) {
47
+ t3 = <div>{prop}</div>;
48
+ $[5] = prop;
49
+ $[6] = t3;
50
+ } else {
51
+ t3 = $[6];
52
+ }
53
+ return t3;
54
+}
55
+
56
+```
57
+
58
+## Logs
59
+
60
+```
61
+{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":105},"end":{"line":9,"column":1,"index":240},"filename":"from-props-setstate-in-effect-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
62
+```
63
+
64
+### Eval output
65
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/from-props-setstate-in-effect-no-error.js
new
+9
@@ -0,0 +1,9 @@
1
+// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
2
+
3
+function Component({setParentState, prop}) {
4
+ useEffect(() => {
5
+ setParentState(prop);
6
+ }, [prop]);
7
+
8
+ return <div>{prop}</div>;
9
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/usestate-derived-from-prop-no-show-in-data-flow-tree.expect.md
-1
@@ -64,7 +64,6 @@ function Component(t0) {
64
## Logs
65
66
```
67
-{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [second]\n\nData Flow Tree:\n└── second (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":14,"column":4,"index":443},"end":{"line":14,"column":8,"index":447},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
67
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":18,"column":1,"index":500},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
68
```
69