[compiler] Don't validate when effect cleanup function depends on effect localized setState state derived values (#35020)
Summary: If we are using a clean up function in an effect and that clean up function depends on a value that is used to set the state we are validating for we shouldn't throw an error since it is a valid use case for an effect. Test Plan: added test --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35020). * #35044 * __->__ #35020
Jorge Cabiedes committed
Nov 10, 2025 at 12:28 UTC
92ac4e8b80cb51a1be7071e8338176680ce8f619
3 files changed
+138
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts
+41
@@ -568,6 +568,26 @@ function renderTree(
568
return result;
569
}
570
571
+function getFnLocalDeps(
572
+ fn: FunctionExpression | undefined,
573
+): Set<IdentifierId> | undefined {
574
+ if (!fn) {
575
+ return undefined;
576
+ }
577
+
578
+ const deps: Set<IdentifierId> = new Set();
579
+
580
+ for (const [, block] of fn.loweredFunc.func.body.blocks) {
581
+ for (const instr of block.instructions) {
582
+ if (instr.value.kind === 'LoadLocal') {
583
+ deps.add(instr.value.place.identifier.id);
584
+ }
585
+ }
586
+ }
587
+
588
+ return deps;
589
+}
590
+
591
function validateEffect(
592
effectFunction: HIRFunction,
593
context: ValidationContext,
@@ -586,8 +606,23 @@ function validateEffect(
606
Set<SourceLocation>
607
> = new Map();
608
609
+ let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
610
+
611
const globals: Set<IdentifierId> = new Set();
612
for (const block of effectFunction.body.blocks.values()) {
613
+ /*
614
+ * if the block is in an effect and is of type return then its an effect's cleanup function
615
+ * if the cleanup function depends on a value from which effect-set state is derived then
616
+ * we can't validate
617
+ */
618
+ if (
619
+ block.terminal.kind === 'return' &&
620
+ block.terminal.returnVariant === 'Explicit'
621
+ ) {
622
+ cleanUpFunctionDeps = getFnLocalDeps(
623
+ context.functions.get(block.terminal.value.identifier.id),
624
+ );
625
+ }
626
for (const pred of block.preds) {
627
if (!seenBlocks.has(pred)) {
628
// skip if block has a back edge
@@ -698,6 +733,12 @@ function validateEffect(
733
),
734
);
735
736
+ for (const dep of derivedSetStateCall.sourceIds) {
737
+ if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
738
+ return;
739
+ }
740
+ }
741
+
742
const propsArr = Array.from(propsSet);
743
const stateArr = Array.from(stateSet);
744
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-with-cleanup-function-depending-on-derived-computation-value.expect.md
new
+76
@@ -0,0 +1,76 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
6
+
7
+import {useEffect, useState} from 'react';
8
+
9
+function Component(file: File) {
10
+ const [imageUrl, setImageUrl] = useState(null);
11
+
12
+ /*
13
+ * Cleaning up the variable or a source of the variable used to setState
14
+ * inside the effect communicates that we always need to clean up something
15
+ * which is a valid use case for useEffect. In which case we want to
16
+ * avoid an throwing
17
+ */
18
+ useEffect(() => {
19
+ const imageUrlPrepared = URL.createObjectURL(file);
20
+ setImageUrl(imageUrlPrepared);
21
+ return () => URL.revokeObjectURL(imageUrlPrepared);
22
+ }, [file]);
23
+
24
+ return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
25
+}
26
+
27
+```
28
+
29
+## Code
30
+
31
+```javascript
32
+import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
33
+
34
+import { useEffect, useState } from "react";
35
+
36
+function Component(file) {
37
+ const $ = _c(5);
38
+ const [imageUrl, setImageUrl] = useState(null);
39
+ let t0;
40
+ let t1;
41
+ if ($[0] !== file) {
42
+ t0 = () => {
43
+ const imageUrlPrepared = URL.createObjectURL(file);
44
+ setImageUrl(imageUrlPrepared);
45
+ return () => URL.revokeObjectURL(imageUrlPrepared);
46
+ };
47
+ t1 = [file];
48
+ $[0] = file;
49
+ $[1] = t0;
50
+ $[2] = t1;
51
+ } else {
52
+ t0 = $[1];
53
+ t1 = $[2];
54
+ }
55
+ useEffect(t0, t1);
56
+ let t2;
57
+ if ($[3] !== imageUrl) {
58
+ t2 = <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
59
+ $[3] = imageUrl;
60
+ $[4] = t2;
61
+ } else {
62
+ t2 = $[4];
63
+ }
64
+ return t2;
65
+}
66
+
67
+```
68
+
69
+## Logs
70
+
71
+```
72
+{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":21,"column":1,"index":700},"filename":"effect-with-cleanup-function-depending-on-derived-computation-value.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
73
+```
74
+
75
+### Eval output
76
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-with-cleanup-function-depending-on-derived-computation-value.js
new
+21
@@ -0,0 +1,21 @@
1
+// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
2
+
3
+import {useEffect, useState} from 'react';
4
+
5
+function Component(file: File) {
6
+ const [imageUrl, setImageUrl] = useState(null);
7
+
8
+ /*
9
+ * Cleaning up the variable or a source of the variable used to setState
10
+ * inside the effect communicates that we always need to clean up something
11
+ * which is a valid use case for useEffect. In which case we want to
12
+ * avoid an throwing
13
+ */
14
+ useEffect(() => {
15
+ const imageUrlPrepared = URL.createObjectURL(file);
16
+ setImageUrl(imageUrlPrepared);
17
+ return () => URL.revokeObjectURL(imageUrlPrepared);
18
+ }, [file]);
19
+
20
+ return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
21
+}