@samitouri / QOS-React / commits / 7d445acb02

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: 364430bbeca4cfca2fbf9df4d92b2e61b3352311 Pull Request resolved: https://github.com/facebook/react/pull/29790

Joe Savona committed Jun 7, 2024 at 12:10 UTC 7d445acb02d3f1011fa231898535c58197d6163a
14 files changed +366 -224
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CollectReactiveIdentifiers.ts
+15
@@ -9,7 +9,9 @@ import {
9 IdentifierId,
10 InstructionId,
11 Place,
12 + PrunedReactiveScopeBlock,
13 ReactiveFunction,
14 + isPrimitiveType,
15 } from "../HIR/HIR";
16 import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
17
@@ -40,6 +42,19 @@ class Visitor extends ReactiveFunctionVisitor<Set<IdentifierId>> {
42 state.add(place.identifier.id);
43 }
44 }
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 + }
58 }
59
60 /*
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-pruned-scope-leaks-value.expect.md deleted
-119
@@ -1,119 +0,0 @@
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 - * Exposes fundamental issue with pruning 'non-reactive' dependencies + flattening
15 - * those scopes. Here, `z`'s original memo block is removed due to the inner hook call.
16 - * However, we also infer that `z` is non-reactive and does not need to be a memo
17 - * dependency.
18 - *
19 - * Current evaluator error:
20 - * Found differences in evaluator results
21 - * Non-forget (expected):
22 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
23 - * [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
24 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
25 - * Forget:
26 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
27 - * [[ (exception in render) Invariant Violation: oh no! ]]
28 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
29 - */
30 -
31 -function MyApp({ count }) {
32 - const z = makeObject_Primitives();
33 - const x = useIdentity(2);
34 - const y = sum(x, count);
35 - mutate(z);
36 - const thing = [y, z];
37 - if (thing[1] !== z) {
38 - invariant(false, "oh no!");
39 - }
40 - return thing;
41 -}
42 -
43 -export const FIXTURE_ENTRYPOINT = {
44 - fn: MyApp,
45 - params: [{ count: 2 }],
46 - sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
47 -};
48 -
49 -```
50 -
51 -## Code
52 -
53 -```javascript
54 -import { c as _c } from "react/compiler-runtime";
55 -import invariant from "invariant";
56 -import {
57 - makeObject_Primitives,
58 - mutate,
59 - sum,
60 - useIdentity,
61 -} from "shared-runtime";
62 -
63 -/**
64 - * Exposes fundamental issue with pruning 'non-reactive' dependencies + flattening
65 - * those scopes. Here, `z`'s original memo block is removed due to the inner hook call.
66 - * However, we also infer that `z` is non-reactive and does not need to be a memo
67 - * dependency.
68 - *
69 - * Current evaluator error:
70 - * Found differences in evaluator results
71 - * Non-forget (expected):
72 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
73 - * [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
74 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
75 - * Forget:
76 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
77 - * [[ (exception in render) Invariant Violation: oh no! ]]
78 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
79 - */
80 -
81 -function MyApp(t0) {
82 - const $ = _c(5);
83 - const { count } = t0;
84 - const z = makeObject_Primitives();
85 - const x = useIdentity(2);
86 - let t1;
87 - if ($[0] !== x || $[1] !== count) {
88 - t1 = sum(x, count);
89 - $[0] = x;
90 - $[1] = count;
91 - $[2] = t1;
92 - } else {
93 - t1 = $[2];
94 - }
95 - const y = t1;
96 - mutate(z);
97 - let t2;
98 - if ($[3] !== y) {
99 - t2 = [y, z];
100 - $[3] = y;
101 - $[4] = t2;
102 - } else {
103 - t2 = $[4];
104 - }
105 - const thing = t2;
106 - if (thing[1] !== z) {
107 - invariant(false, "oh no!");
108 - }
109 - return thing;
110 -}
111 -
112 -export const FIXTURE_ENTRYPOINT = {
113 - fn: MyApp,
114 - params: [{ count: 2 }],
115 - sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
116 -};
117 -
118 -```
119 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-pruned-scope-leaks-value.ts deleted
-43
@@ -1,43 +0,0 @@
1 -import invariant from "invariant";
2 -import {
3 - makeObject_Primitives,
4 - mutate,
5 - sum,
6 - useIdentity,
7 -} from "shared-runtime";
8 -
9 -/**
10 - * Exposes fundamental issue with pruning 'non-reactive' dependencies + flattening
11 - * those scopes. Here, `z`'s original memo block is removed due to the inner hook call.
12 - * However, we also infer that `z` is non-reactive and does not need to be a memo
13 - * dependency.
14 - *
15 - * Current evaluator error:
16 - * Found differences in evaluator results
17 - * Non-forget (expected):
18 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
19 - * [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
20 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
21 - * Forget:
22 - * (kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
23 - * [[ (exception in render) Invariant Violation: oh no! ]]
24 - * [5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
25 - */
26 -
27 -function MyApp({ count }) {
28 - const z = makeObject_Primitives();
29 - const x = useIdentity(2);
30 - const y = sum(x, count);
31 - mutate(z);
32 - const thing = [y, z];
33 - if (thing[1] !== z) {
34 - invariant(false, "oh no!");
35 - }
36 - return thing;
37 -}
38 -
39 -export const FIXTURE_ENTRYPOINT = {
40 - fn: MyApp,
41 - params: [{ count: 2 }],
42 - sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
43 -};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-control-dependency-from-interleaved-reactivity-for-of.expect.md
+5 -4
@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
39 ```javascript
40 import { c as _c } from "react/compiler-runtime";
41 function Component(props) {
42 - const $ = _c(1);
42 + const $ = _c(2);
43
44 const a = [];
45 const b = [];
@@ -53,11 +53,12 @@ function Component(props) {
53 x = 1;
54 }
55 let t0;
56 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
56 + if ($[0] !== x) {
57 t0 = [x];
58 - $[0] = t0;
58 + $[0] = x;
59 + $[1] = t0;
60 } else {
60 - t0 = $[0];
61 + t0 = $[1];
62 }
63 return t0;
64 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-dont-memoize-array-with-mutable-map-after-hook.expect.md
+11 -9
@@ -39,7 +39,7 @@ import { useEffect, useState } from "react";
39 import { mutate } from "shared-runtime";
40
41 function Component(props) {
42 - const $ = _c(6);
42 + const $ = _c(8);
43 const x = [{ ...props.value }];
44 let t0;
45 let t1;
@@ -64,25 +64,27 @@ function Component(props) {
64 return <span key={item.id}>{item.text}</span>;
65 });
66 let t3;
67 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
67 + if ($[2] !== y) {
68 t3 = mutate(y);
69 - $[2] = t3;
69 + $[2] = y;
70 + $[3] = t3;
71 } else {
71 - t3 = $[2];
72 + t3 = $[3];
73 }
74 let t4;
74 - if ($[3] !== onClick || $[4] !== t2) {
75 + if ($[4] !== onClick || $[5] !== t2 || $[6] !== t3) {
76 t4 = (
77 <div onClick={onClick}>
78 {t2}
79 {t3}
80 </div>
81 );
81 - $[3] = onClick;
82 - $[4] = t2;
83 - $[5] = t4;
82 + $[4] = onClick;
83 + $[5] = t2;
84 + $[6] = t3;
85 + $[7] = t4;
86 } else {
85 - t4 = $[5];
87 + t4 = $[7];
88 }
89 return t4;
90 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-pruned-scope-leaks-value-via-alias.expect.md new
+112
@@ -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"}]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-pruned-scope-leaks-value-via-alias.ts new
+37
@@ -0,0 +1,37 @@
1 +import invariant from "invariant";
2 +import {
3 + makeObject_Primitives,
4 + mutate,
5 + sum,
6 + useIdentity,
7 +} from "shared-runtime";
8 +
9 +/**
10 + * Here, `z`'s original memo block is removed due to the inner hook call.
11 + * However, we also infer that `z` is non-reactive, so by default we would create
12 + * the memo block for `thing = [y, z]` as only depending on `y`.
13 + *
14 + * This could then mean that `thing[1]` and `z` may not refer to the same value,
15 + * since z recreates every time but `thing` doesn't correspondingly invalidate.
16 + *
17 + * The fix is to consider pruned memo block outputs as reactive, since they will
18 + * recreate on every render. This means `thing` depends on both y and z.
19 + */
20 +function MyApp({ count }) {
21 + const z = makeObject_Primitives();
22 + const x = useIdentity(2);
23 + const y = sum(x, count);
24 + mutate(z);
25 + const z2 = z;
26 + const thing = [y, z2];
27 + if (thing[1] !== z) {
28 + invariant(false, "oh no!");
29 + }
30 + return thing;
31 +}
32 +
33 +export const FIXTURE_ENTRYPOINT = {
34 + fn: MyApp,
35 + params: [{ count: 2 }],
36 + sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
37 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-pruned-scope-leaks-value.expect.md new
+110
@@ -0,0 +1,110 @@
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 thing = [y, z];
30 + if (thing[1] !== z) {
31 + invariant(false, "oh no!");
32 + }
33 + return thing;
34 +}
35 +
36 +export const FIXTURE_ENTRYPOINT = {
37 + fn: MyApp,
38 + params: [{ count: 2 }],
39 + sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
40 +};
41 +
42 +```
43 +
44 +## Code
45 +
46 +```javascript
47 +import { c as _c } from "react/compiler-runtime";
48 +import invariant from "invariant";
49 +import {
50 + makeObject_Primitives,
51 + mutate,
52 + sum,
53 + useIdentity,
54 +} from "shared-runtime";
55 +
56 +/**
57 + * Here, `z`'s original memo block is removed due to the inner hook call.
58 + * However, we also infer that `z` is non-reactive, so by default we would create
59 + * the memo block for `thing = [y, z]` as only depending on `y`.
60 + *
61 + * This could then mean that `thing[1]` and `z` may not refer to the same value,
62 + * since z recreates every time but `thing` doesn't correspondingly invalidate.
63 + *
64 + * The fix is to consider pruned memo block outputs as reactive, since they will
65 + * recreate on every render. This means `thing` depends on both y and z.
66 + */
67 +function MyApp(t0) {
68 + const $ = _c(6);
69 + const { count } = t0;
70 + const z = makeObject_Primitives();
71 + const x = useIdentity(2);
72 + let t1;
73 + if ($[0] !== x || $[1] !== count) {
74 + t1 = sum(x, count);
75 + $[0] = x;
76 + $[1] = count;
77 + $[2] = t1;
78 + } else {
79 + t1 = $[2];
80 + }
81 + const y = t1;
82 + mutate(z);
83 + let t2;
84 + if ($[3] !== y || $[4] !== z) {
85 + t2 = [y, z];
86 + $[3] = y;
87 + $[4] = z;
88 + $[5] = t2;
89 + } else {
90 + t2 = $[5];
91 + }
92 + const thing = t2;
93 + if (thing[1] !== z) {
94 + invariant(false, "oh no!");
95 + }
96 + return thing;
97 +}
98 +
99 +export const FIXTURE_ENTRYPOINT = {
100 + fn: MyApp,
101 + params: [{ count: 2 }],
102 + sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
103 +};
104 +
105 +```
106 +
107 +### Eval output
108 +(kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
109 +[4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
110 +[5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-pruned-scope-leaks-value.ts new
+36
@@ -0,0 +1,36 @@
1 +import invariant from "invariant";
2 +import {
3 + makeObject_Primitives,
4 + mutate,
5 + sum,
6 + useIdentity,
7 +} from "shared-runtime";
8 +
9 +/**
10 + * Here, `z`'s original memo block is removed due to the inner hook call.
11 + * However, we also infer that `z` is non-reactive, so by default we would create
12 + * the memo block for `thing = [y, z]` as only depending on `y`.
13 + *
14 + * This could then mean that `thing[1]` and `z` may not refer to the same value,
15 + * since z recreates every time but `thing` doesn't correspondingly invalidate.
16 + *
17 + * The fix is to consider pruned memo block outputs as reactive, since they will
18 + * recreate on every render. This means `thing` depends on both y and z.
19 + */
20 +function MyApp({ count }) {
21 + const z = makeObject_Primitives();
22 + const x = useIdentity(2);
23 + const y = sum(x, count);
24 + mutate(z);
25 + const thing = [y, z];
26 + if (thing[1] !== z) {
27 + invariant(false, "oh no!");
28 + }
29 + return thing;
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: MyApp,
34 + params: [{ count: 2 }],
35 + sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
36 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-reactivity-value-block.expect.md renamed
+26 -23
@@ -8,17 +8,16 @@ import {
8 makeObject_Primitives,
9 useNoAlias,
10 } from "shared-runtime";
11 +
12 /**
12 - * BUG
13 - * Found differences in evaluator results
14 - * Non-forget (expected):
15 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
16 - * [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
17 - * Forget:
18 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
19 - * [[ (exception in render) Error: Oh no! ]]
13 + * Here the scope for `obj` is pruned because it spans the `useNoAlias()` hook call.
14 + * Because `obj` is non-reactive, it would by default be excluded as dependency for
15 + * `result = [...identity(obj)..., obj]`, but this could then cause the values in
16 + * `result` to be out of sync with `obj`.
17 + *
18 + * The fix is to consider pruned memo block outputs as reactive, since they will
19 + * recreate on every render. This means `thing` depends on both y and z.
20 */
21 -
21 function Foo() {
22 const obj = makeObject_Primitives();
23 // hook calls keeps the next two lines as its own reactive scope
@@ -53,19 +52,18 @@ import {
52 makeObject_Primitives,
53 useNoAlias,
54 } from "shared-runtime";
55 +
56 /**
57 - * BUG
58 - * Found differences in evaluator results
59 - * Non-forget (expected):
60 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
61 - * [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
62 - * Forget:
63 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
64 - * [[ (exception in render) Error: Oh no! ]]
57 + * Here the scope for `obj` is pruned because it spans the `useNoAlias()` hook call.
58 + * Because `obj` is non-reactive, it would by default be excluded as dependency for
59 + * `result = [...identity(obj)..., obj]`, but this could then cause the values in
60 + * `result` to be out of sync with `obj`.
61 + *
62 + * The fix is to consider pruned memo block outputs as reactive, since they will
63 + * recreate on every render. This means `thing` depends on both y and z.
64 */
66 -
65 function Foo() {
68 - const $ = _c(1);
66 + const $ = _c(3);
67 const obj = makeObject_Primitives();
68
69 useNoAlias();
@@ -73,11 +71,13 @@ function Foo() {
71 const shouldCaptureObj = obj != null && CONST_TRUE;
72 const t0 = shouldCaptureObj ? identity(obj) : null;
73 let t1;
76 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
74 + if ($[0] !== t0 || $[1] !== obj) {
75 t1 = [t0, obj];
78 - $[0] = t1;
76 + $[0] = t0;
77 + $[1] = obj;
78 + $[2] = t1;
79 } else {
80 - t1 = $[0];
80 + t1 = $[2];
81 }
82 const result = t1;
83
@@ -95,4 +95,7 @@ export const FIXTURE_ENTRYPOINT = {
95 };
96
97 ```
98 -
\ No newline at end of file
98 +
99 +### Eval output
100 +(kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
101 +[{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-reactivity-value-block.ts renamed
+8 -9
@@ -4,17 +4,16 @@ import {
4 makeObject_Primitives,
5 useNoAlias,
6 } from "shared-runtime";
7 +
8 /**
8 - * BUG
9 - * Found differences in evaluator results
10 - * Non-forget (expected):
11 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
12 - * [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
13 - * Forget:
14 - * (kind: ok) [{"a":0,"b":"value1","c":true},"[[ cyclic ref *1 ]]"]
15 - * [[ (exception in render) Error: Oh no! ]]
9 + * Here the scope for `obj` is pruned because it spans the `useNoAlias()` hook call.
10 + * Because `obj` is non-reactive, it would by default be excluded as dependency for
11 + * `result = [...identity(obj)..., obj]`, but this could then cause the values in
12 + * `result` to be out of sync with `obj`.
13 + *
14 + * The fix is to consider pruned memo block outputs as reactive, since they will
15 + * recreate on every render. This means `thing` depends on both y and z.
16 */
17 -
17 function Foo() {
18 const obj = makeObject_Primitives();
19 // hook calls keeps the next two lines as its own reactive scope
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unmemoized-nonreactive-dependency-is-pruned-as-dependency.expect.md
+1 -11
@@ -25,23 +25,13 @@ export const FIXTURE_ENTRYPOINT = {
25 ## Code
26
27 ```javascript
28 -import { c as _c } from "react/compiler-runtime";
28 import { mutate, useNoAlias } from "shared-runtime";
29
30 function Component(props) {
32 - const $ = _c(1);
33 -
31 const x = [];
32 useNoAlias();
33 mutate(x);
37 - let t0;
38 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
39 - t0 = <div>{x}</div>;
40 - $[0] = t0;
41 - } else {
42 - t0 = $[0];
43 - }
44 - return t0;
34 + return <div>{x}</div>;
35 }
36
37 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-call-second-function-which-captures-maybe-mutable-value-dont-preserve-memoization.expect.md
+5 -4
@@ -49,7 +49,7 @@ import {
49 } from "shared-runtime";
50
51 function Component(props) {
52 - const $ = _c(1);
52 + const $ = _c(2);
53 const object = makeObject_Primitives();
54
55 useHook();
@@ -64,11 +64,12 @@ function Component(props) {
64
65 identity(object);
66 let t0;
67 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
67 + if ($[0] !== onClick) {
68 t0 = <div onClick={onClick} />;
69 - $[0] = t0;
69 + $[0] = onClick;
70 + $[1] = t0;
71 } else {
71 - t0 = $[0];
72 + t0 = $[1];
73 }
74 return t0;
75 }
compiler/packages/snap/src/SproutTodoFilter.ts
-2
@@ -485,8 +485,6 @@ const skipFilter = new Set([
485 "rules-of-hooks/rules-of-hooks-69521d94fa03",
486
487 // bugs
488 - "bug-invalid-reactivity-value-block",
489 - "bug-invalid-pruned-scope-leaks-value",
488 "bug-invalid-hoisting-functionexpr",
489 "original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",
490 "original-reactive-scopes-fork/bug-hoisted-declaration-with-scope",