Skip to content

Commit 4624e46

Browse files
committed
compiler: treat pruned scope outputs as reactive
Mostly addresses the issue with non-reactive pruned scopes. Before, values from pruned scopes would not be memoized, but could still be depended upon by downstream scopes. However, those downstream scopes would assume the value could never change. This could allow the developer to observe two different versions of a value - the freshly created one (if observed outside a scope) or a cached one (if observed inside, or through) a scope which used the value but didn't depend on it. The fix here is to consider the outputs of pruned reactive scopes as reactive. Note that this is a partial fix because of things like control variables — the full solution would be to mark these values as reactive, and then re-run InferReactivePlaces. We can do this once we've fully converted our pipeline to use HIR everywhere. For now, this should fix most issues in practice because PruneNonReactiveDependencies already does basic alias tracking (see new fixture). ghstack-source-id: 11f48618d0413075c43ee88529e00e8e523e61f7 Pull Request resolved: #29790
1 parent 90ac345 commit 4624e46

14 files changed

+366
-224
lines changed

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CollectReactiveIdentifiers.ts

+15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
IdentifierId,
1010
InstructionId,
1111
Place,
12+
PrunedReactiveScopeBlock,
1213
ReactiveFunction,
14+
isPrimitiveType,
1315
} from "../HIR/HIR";
1416
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
1517

@@ -40,6 +42,19 @@ class Visitor extends ReactiveFunctionVisitor<Set<IdentifierId>> {
4042
state.add(place.identifier.id);
4143
}
4244
}
45+
46+
override visitPrunedScope(
47+
scopeBlock: PrunedReactiveScopeBlock,
48+
state: Set<IdentifierId>
49+
): void {
50+
this.traversePrunedScope(scopeBlock, state);
51+
52+
for (const [id, decl] of scopeBlock.scope.declarations) {
53+
if (!isPrimitiveType(decl.identifier)) {
54+
state.add(id);
55+
}
56+
}
57+
}
4358
}
4459

4560
/*

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-pruned-scope-leaks-value.expect.md

-119
This file was deleted.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-pruned-scope-leaks-value.ts

-43
This file was deleted.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-control-dependency-from-interleaved-reactivity-for-of.expect.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
3939
```javascript
4040
import { c as _c } from "react/compiler-runtime";
4141
function Component(props) {
42-
const $ = _c(1);
42+
const $ = _c(2);
4343

4444
const a = [];
4545
const b = [];
@@ -53,11 +53,12 @@ function Component(props) {
5353
x = 1;
5454
}
5555
let t0;
56-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
56+
if ($[0] !== x) {
5757
t0 = [x];
58-
$[0] = t0;
58+
$[0] = x;
59+
$[1] = t0;
5960
} else {
60-
t0 = $[0];
61+
t0 = $[1];
6162
}
6263
return t0;
6364
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-dont-memoize-array-with-mutable-map-after-hook.expect.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { useEffect, useState } from "react";
3939
import { mutate } from "shared-runtime";
4040

4141
function Component(props) {
42-
const $ = _c(6);
42+
const $ = _c(8);
4343
const x = [{ ...props.value }];
4444
let t0;
4545
let t1;
@@ -64,25 +64,27 @@ function Component(props) {
6464
return <span key={item.id}>{item.text}</span>;
6565
});
6666
let t3;
67-
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
67+
if ($[2] !== y) {
6868
t3 = mutate(y);
69-
$[2] = t3;
69+
$[2] = y;
70+
$[3] = t3;
7071
} else {
71-
t3 = $[2];
72+
t3 = $[3];
7273
}
7374
let t4;
74-
if ($[3] !== onClick || $[4] !== t2) {
75+
if ($[4] !== onClick || $[5] !== t2 || $[6] !== t3) {
7576
t4 = (
7677
<div onClick={onClick}>
7778
{t2}
7879
{t3}
7980
</div>
8081
);
81-
$[3] = onClick;
82-
$[4] = t2;
83-
$[5] = t4;
82+
$[4] = onClick;
83+
$[5] = t2;
84+
$[6] = t3;
85+
$[7] = t4;
8486
} else {
85-
t4 = $[5];
87+
t4 = $[7];
8688
}
8789
return t4;
8890
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
2+
## Input
3+
4+
```javascript
5+
import invariant from "invariant";
6+
import {
7+
makeObject_Primitives,
8+
mutate,
9+
sum,
10+
useIdentity,
11+
} from "shared-runtime";
12+
13+
/**
14+
* Here, `z`'s original memo block is removed due to the inner hook call.
15+
* However, we also infer that `z` is non-reactive, so by default we would create
16+
* the memo block for `thing = [y, z]` as only depending on `y`.
17+
*
18+
* This could then mean that `thing[1]` and `z` may not refer to the same value,
19+
* since z recreates every time but `thing` doesn't correspondingly invalidate.
20+
*
21+
* The fix is to consider pruned memo block outputs as reactive, since they will
22+
* recreate on every render. This means `thing` depends on both y and z.
23+
*/
24+
function MyApp({ count }) {
25+
const z = makeObject_Primitives();
26+
const x = useIdentity(2);
27+
const y = sum(x, count);
28+
mutate(z);
29+
const z2 = z;
30+
const thing = [y, z2];
31+
if (thing[1] !== z) {
32+
invariant(false, "oh no!");
33+
}
34+
return thing;
35+
}
36+
37+
export const FIXTURE_ENTRYPOINT = {
38+
fn: MyApp,
39+
params: [{ count: 2 }],
40+
sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
41+
};
42+
43+
```
44+
45+
## Code
46+
47+
```javascript
48+
import { c as _c } from "react/compiler-runtime";
49+
import invariant from "invariant";
50+
import {
51+
makeObject_Primitives,
52+
mutate,
53+
sum,
54+
useIdentity,
55+
} from "shared-runtime";
56+
57+
/**
58+
* Here, `z`'s original memo block is removed due to the inner hook call.
59+
* However, we also infer that `z` is non-reactive, so by default we would create
60+
* the memo block for `thing = [y, z]` as only depending on `y`.
61+
*
62+
* This could then mean that `thing[1]` and `z` may not refer to the same value,
63+
* since z recreates every time but `thing` doesn't correspondingly invalidate.
64+
*
65+
* The fix is to consider pruned memo block outputs as reactive, since they will
66+
* recreate on every render. This means `thing` depends on both y and z.
67+
*/
68+
function MyApp(t0) {
69+
const $ = _c(6);
70+
const { count } = t0;
71+
const z = makeObject_Primitives();
72+
const x = useIdentity(2);
73+
let t1;
74+
if ($[0] !== x || $[1] !== count) {
75+
t1 = sum(x, count);
76+
$[0] = x;
77+
$[1] = count;
78+
$[2] = t1;
79+
} else {
80+
t1 = $[2];
81+
}
82+
const y = t1;
83+
mutate(z);
84+
const z2 = z;
85+
let t2;
86+
if ($[3] !== y || $[4] !== z2) {
87+
t2 = [y, z2];
88+
$[3] = y;
89+
$[4] = z2;
90+
$[5] = t2;
91+
} else {
92+
t2 = $[5];
93+
}
94+
const thing = t2;
95+
if (thing[1] !== z) {
96+
invariant(false, "oh no!");
97+
}
98+
return thing;
99+
}
100+
101+
export const FIXTURE_ENTRYPOINT = {
102+
fn: MyApp,
103+
params: [{ count: 2 }],
104+
sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
105+
};
106+
107+
```
108+
109+
### Eval output
110+
(kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
111+
[4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
112+
[5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]

0 commit comments

Comments
 (0)