Skip to content

Commit b421099

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

File tree

3 files changed

+187
-47
lines changed

3 files changed

+187
-47
lines changed

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

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -467,53 +467,52 @@ type TreeNode = {
467467
function buildTreeNode(
468468
sourceId: IdentifierId,
469469
context: ValidationContext,
470-
): TreeNode | null {
470+
visited: Set<string> = new Set(),
471+
): Array<TreeNode> {
471472
const sourceMetadata = context.derivationCache.cache.get(sourceId);
472473
if (!sourceMetadata) {
473-
return null;
474+
return [];
474475
}
475476

476-
const childSourceIds = Array.from(sourceMetadata.sourcesIds).filter(
477-
id => id !== sourceId,
478-
);
479-
480-
if (!isNamedIdentifier(sourceMetadata.place)) {
481-
const childrenMap = new Map<string, TreeNode>();
482-
for (const childId of childSourceIds) {
483-
const childNode = buildTreeNode(childId, context);
484-
if (childNode) {
485-
if (!childrenMap.has(childNode.name)) {
486-
childrenMap.set(childNode.name, childNode);
477+
const children: Array<TreeNode> = [];
478+
479+
const namedSiblings: Set<string> = new Set();
480+
for (const childId of sourceMetadata.sourcesIds) {
481+
const childNodes = buildTreeNode(
482+
childId,
483+
context,
484+
new Set([
485+
...visited,
486+
...(isNamedIdentifier(sourceMetadata.place)
487+
? [sourceMetadata.place.identifier.name.value]
488+
: []),
489+
]),
490+
);
491+
if (childNodes) {
492+
for (const childNode of childNodes) {
493+
if (!namedSiblings.has(childNode.name)) {
494+
children.push(childNode);
495+
namedSiblings.add(childNode.name);
487496
}
488497
}
489498
}
490-
const children = Array.from(childrenMap.values());
491-
492-
if (children.length === 1) {
493-
return children[0];
494-
} else if (children.length > 1) {
495-
return null;
496-
}
497-
return null;
498499
}
499500

500-
const childrenMap = new Map<string, TreeNode>();
501-
for (const childId of childSourceIds) {
502-
const childNode = buildTreeNode(childId, context);
503-
if (childNode) {
504-
if (!childrenMap.has(childNode.name)) {
505-
childrenMap.set(childNode.name, childNode);
506-
}
507-
}
501+
if (
502+
isNamedIdentifier(sourceMetadata.place) &&
503+
!visited.has(sourceMetadata.place.identifier.name.value)
504+
) {
505+
return [
506+
{
507+
name: sourceMetadata.place.identifier.name.value,
508+
typeOfValue: sourceMetadata.typeOfValue,
509+
isSource: sourceMetadata.isStateSource,
510+
children: children,
511+
},
512+
];
508513
}
509-
const children = Array.from(childrenMap.values());
510514

511-
return {
512-
name: sourceMetadata.place.identifier.name.value,
513-
typeOfValue: sourceMetadata.typeOfValue,
514-
isSource: sourceMetadata.isStateSource,
515-
children,
516-
};
515+
return children;
517516
}
518517

519518
function renderTree(
@@ -558,6 +557,23 @@ function renderTree(
558557
return result;
559558
}
560559

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

595+
const cleanUpFunctionDeps: Set<IdentifierId> = new Set();
596+
579597
const globals: Set<IdentifierId> = new Set();
580598
for (const block of effectFunction.body.blocks.values()) {
599+
/*
600+
* if the block is in an effect and is of type return then its an effect's cleanup function
601+
* if the cleanup function depends on a value from which effect-set state is derived then
602+
* we can't validate
603+
*/
604+
if (
605+
block.terminal.kind === 'return' &&
606+
block.terminal.returnVariant === 'Explicit'
607+
) {
608+
getFnGlobalDeps(
609+
context.functions.get(block.terminal.value.identifier.id),
610+
cleanUpFunctionDeps,
611+
);
612+
}
581613
for (const pred of block.preds) {
582614
if (!seenBlocks.has(pred)) {
583615
// skip if block has a back edge
@@ -652,6 +684,8 @@ function validateEffect(
652684
}
653685

654686
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
687+
console.log(context.derivationCache.cache);
688+
console.log(derivedSetStateCall);
655689
const rootSetStateCall = getRootSetState(
656690
derivedSetStateCall.id,
657691
context.setStateLoads,
@@ -667,25 +701,34 @@ function validateEffect(
667701
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
668702
const propsSet = new Set<string>();
669703
const stateSet = new Set<string>();
704+
const allSetStateDeps = new Set<IdentifierId>();
670705

671-
const treeNodesMap = new Map<string, TreeNode>();
706+
const rootNodesMap = new Map<string, TreeNode>();
672707
for (const id of allSourceIds) {
673-
const node = buildTreeNode(id, context);
674-
if (node && !treeNodesMap.has(node.name)) {
675-
treeNodesMap.set(node.name, node);
708+
const nodes = buildTreeNode(id, context);
709+
for (const node of nodes) {
710+
if (!rootNodesMap.has(node.name)) {
711+
rootNodesMap.set(node.name, node);
712+
}
676713
}
677714
}
678-
const treeNodes = Array.from(treeNodesMap.values());
715+
const rootNodes = Array.from(rootNodesMap.values());
679716

680-
const trees = treeNodes.map((node, index) =>
681-
renderTree(
717+
const trees = rootNodes.map((node, index) => {
718+
return renderTree(
682719
node,
683720
'',
684-
index === treeNodes.length - 1,
721+
index === rootNodes.length - 1,
685722
propsSet,
686723
stateSet,
687-
),
688-
);
724+
);
725+
});
726+
727+
for (const dep of allSetStateDeps) {
728+
if (cleanUpFunctionDeps.has(dep)) {
729+
return;
730+
}
731+
}
689732

690733
const propsArr = Array.from(propsSet);
691734
const stateArr = Array.from(stateSet);
@@ -699,7 +742,7 @@ function validateEffect(
699742
rootSources += `State: [${stateArr.join(', ')}]`;
700743
}
701744

702-
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
745+
const description = `TESTUsing an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
703746
704747
This setState call is setting a derived value that depends on the following reactive sources:
705748
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)