[optim] All nested properties in refs are ref values
Forget currently removes memoization of callbacks that have `mutate` effects on `ref` inner properties. @gsathya pointed out that our existing compiler behavior is to (1) NOT extend mutable ranges for functions that mutate `ref.current` and (2) extend mutable ranges for functions that mutate `ref.current.inner`. ```js // input function Component() { const ref = useRef({ text: null }); const handleChange = useCallback((e) => { ref.current.text = e.target.value; }); return <input onChange={handleChange} />; } // output function Component() { const ref = useRef({ text: null }); // now unmemoized! const handleChange = (e) => { ref.current.text = e.target.value; };
Mofei Zhang committed
Jan 29, 2024 at 17:04 UTC
74d8a18637587334a011d572afa52c3c60382611
6 files changed
+199
-10
compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
+3
-1
@@ -389,7 +389,9 @@ addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
389
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
390
]);
391
392
-addObject(BUILTIN_SHAPES, BuiltInRefValueId, []);
392
+addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
393
+ ["*", { kind: "Object", shapeId: BuiltInRefValueId }],
394
+]);
395
396
addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
397
[
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-current-field-write-not-added-to-dep.expect.md
new
+67
@@ -0,0 +1,67 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useRef } from "react";
6
+
7
+function Component() {
8
+ const ref = useRef({ text: { value: null } });
9
+ const inputChanged = (e) => {
10
+ ref.current.text.value = e.target.value;
11
+ };
12
+
13
+ return <input onChange={inputChanged} />;
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Component,
18
+ params: [{}],
19
+};
20
+
21
+```
22
+
23
+## Code
24
+
25
+```javascript
26
+import { useRef, unstable_useMemoCache as useMemoCache } from "react";
27
+
28
+function Component() {
29
+ const $ = useMemoCache(4);
30
+ let t0;
31
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32
+ t0 = { text: { value: null } };
33
+ $[0] = t0;
34
+ } else {
35
+ t0 = $[0];
36
+ }
37
+ const ref = useRef(t0);
38
+ let t1;
39
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
40
+ t1 = (e) => {
41
+ ref.current.text.value = e.target.value;
42
+ };
43
+ $[1] = t1;
44
+ } else {
45
+ t1 = $[1];
46
+ }
47
+ const inputChanged = t1;
48
+ let t2;
49
+ if ($[2] !== inputChanged) {
50
+ t2 = <input onChange={inputChanged} />;
51
+ $[2] = inputChanged;
52
+ $[3] = t2;
53
+ } else {
54
+ t2 = $[3];
55
+ }
56
+ return t2;
57
+}
58
+
59
+export const FIXTURE_ENTRYPOINT = {
60
+ fn: Component,
61
+ params: [{}],
62
+};
63
+
64
+```
65
+
66
+### Eval output
67
+(kind: ok) <input>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-current-field-write-not-added-to-dep.js
new
+15
@@ -0,0 +1,15 @@
1
+import { useRef } from "react";
2
+
3
+function Component() {
4
+ const ref = useRef({ text: { value: null } });
5
+ const inputChanged = (e) => {
6
+ ref.current.text.value = e.target.value;
7
+ };
8
+
9
+ return <input onChange={inputChanged} />;
10
+}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: Component,
14
+ params: [{}],
15
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md
+16
-9
@@ -37,23 +37,30 @@ import {
37
} from "react";
38
39
function Component(props) {
40
- const $ = useMemoCache(2);
41
- const ref = useRef({ inner: null });
40
+ const $ = useMemoCache(3);
41
+ let t0;
42
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43
+ t0 = { inner: null };
44
+ $[0] = t0;
45
+ } else {
46
+ t0 = $[0];
47
+ }
48
+ const ref = useRef(t0);
49
50
const onChange = (event) => {
51
ref.current.inner = event.target.value;
52
};
53
54
ref.current.inner = null;
48
- let t0;
49
- if ($[0] !== onChange) {
50
- t0 = <input onChange={onChange} />;
51
- $[0] = onChange;
52
- $[1] = t0;
55
+ let t1;
56
+ if ($[1] !== onChange) {
57
+ t1 = <input onChange={onChange} />;
58
+ $[1] = onChange;
59
+ $[2] = t1;
60
} else {
54
- t0 = $[1];
61
+ t1 = $[2];
62
}
56
- return t0;
63
+ return t1;
64
}
65
66
export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property.expect.md
new
+78
@@ -0,0 +1,78 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useCallback, useRef } from "react";
6
+
7
+// Identical to useCallback-set-ref-nested-property-preserve-memoization,
8
+// but with a different set of compiler flags
9
+function Component({}) {
10
+ const ref = useRef({ inner: null });
11
+
12
+ const onChange = useCallback((event) => {
13
+ // The ref should still be mutable here even though function deps are frozen in
14
+ // @enablePreserveExistingMemoizationGuarantees mode
15
+ ref.current.inner = event.target.value;
16
+ });
17
+
18
+ return <input onChange={onChange} />;
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: Component,
23
+ params: [{}],
24
+};
25
+
26
+```
27
+
28
+## Code
29
+
30
+```javascript
31
+import {
32
+ useCallback,
33
+ useRef,
34
+ unstable_useMemoCache as useMemoCache,
35
+} from "react";
36
+
37
+// Identical to useCallback-set-ref-nested-property-preserve-memoization,
38
+// but with a different set of compiler flags
39
+function Component(t27) {
40
+ const $ = useMemoCache(4);
41
+ let t0;
42
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43
+ t0 = { inner: null };
44
+ $[0] = t0;
45
+ } else {
46
+ t0 = $[0];
47
+ }
48
+ const ref = useRef(t0);
49
+ let t1;
50
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
51
+ t1 = (event) => {
52
+ ref.current.inner = event.target.value;
53
+ };
54
+ $[1] = t1;
55
+ } else {
56
+ t1 = $[1];
57
+ }
58
+ const onChange = t1;
59
+ let t2;
60
+ if ($[2] !== onChange) {
61
+ t2 = <input onChange={onChange} />;
62
+ $[2] = onChange;
63
+ $[3] = t2;
64
+ } else {
65
+ t2 = $[3];
66
+ }
67
+ return t2;
68
+}
69
+
70
+export const FIXTURE_ENTRYPOINT = {
71
+ fn: Component,
72
+ params: [{}],
73
+};
74
+
75
+```
76
+
77
+### Eval output
78
+(kind: ok) <input>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property.js
new
+20
@@ -0,0 +1,20 @@
1
+import { useCallback, useRef } from "react";
2
+
3
+// Identical to useCallback-set-ref-nested-property-preserve-memoization,
4
+// but with a different set of compiler flags
5
+function Component({}) {
6
+ const ref = useRef({ inner: null });
7
+
8
+ const onChange = useCallback((event) => {
9
+ // The ref should still be mutable here even though function deps are frozen in
10
+ // @enablePreserveExistingMemoizationGuarantees mode
11
+ ref.current.inner = event.target.value;
12
+ });
13
+
14
+ return <input onChange={onChange} />;
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Component,
19
+ params: [{}],
20
+};