@samitouri / QOS-React-2 / commits / 92ac4e8b80

[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 +}