Skip to content

Commit 6791143

Browse files
committed
[compiler] Don't validate when effect cleanup function depends on effect localized setState state derived values
1 parent 557b4f1 commit 6791143

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,23 @@ function renderTree(
558558
return result;
559559
}
560560

561+
function getFnGlobalDeps(
562+
fn: FunctionExpression | undefined,
563+
deps: Set<IdentifierId>,
564+
): void {
565+
if (!fn) {
566+
return;
567+
}
568+
569+
for (const [, block] of fn.loweredFunc.func.body.blocks) {
570+
for (const instr of block.instructions) {
571+
if (instr.value.kind === 'LoadLocal') {
572+
deps.add(instr.value.place.identifier.id);
573+
}
574+
}
575+
}
576+
}
577+
561578
function validateEffect(
562579
effectFunction: HIRFunction,
563580
context: ValidationContext,
@@ -576,8 +593,24 @@ function validateEffect(
576593
Set<SourceLocation>
577594
> = new Map();
578595

596+
const cleanUpFunctionDeps: Set<IdentifierId> = new Set();
597+
579598
const globals: Set<IdentifierId> = new Set();
580599
for (const block of effectFunction.body.blocks.values()) {
600+
/*
601+
* if the block is in an effect and is of type return then its an effect's cleanup function
602+
* if the cleanup function depends on a value from which effect-set state is derived then
603+
* we can't validate
604+
*/
605+
if (
606+
block.terminal.kind === 'return' &&
607+
block.terminal.returnVariant === 'Explicit'
608+
) {
609+
getFnGlobalDeps(
610+
context.functions.get(block.terminal.value.identifier.id),
611+
cleanUpFunctionDeps,
612+
);
613+
}
581614
for (const pred of block.preds) {
582615
if (!seenBlocks.has(pred)) {
583616
// skip if block has a back edge
@@ -667,6 +700,7 @@ function validateEffect(
667700
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
668701
const propsSet = new Set<string>();
669702
const stateSet = new Set<string>();
703+
const allSetStateDeps = new Set<IdentifierId>();
670704

671705
const treeNodesMap = new Map<string, TreeNode>();
672706
for (const id of allSourceIds) {
@@ -687,6 +721,12 @@ function validateEffect(
687721
),
688722
);
689723

724+
for (const dep of allSetStateDeps) {
725+
if (cleanUpFunctionDeps.has(dep)) {
726+
return;
727+
}
728+
}
729+
690730
const propsArr = Array.from(propsSet);
691731
const stateArr = Array.from(stateSet);
692732

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)