Repro for unmemoized array due to mutation surrounding hook
Distilled repro of an internal example we found. Forget determines a mutable range for the array, but that mutable range spans a hook call, so the reactive scope gets pruned. That's all working as expected. What isn't ideal though is that if we know `x` is an array and `f` can't mutate its arguments, then `x.map(f)` shouldn't count as a mutation of `x`, since Array.prototype.map can only mutate the receiver via the callback (if the callback mutates its args). Improving on this example requires a) we have to know it's an Array, via type information or bc we saw an array literal and b) being precise about which functions could possibly mutate their parameters, which is tricky because of indirect mutations via stores, etc.
Joe Savona committed
Nov 29, 2023 at 10:46 UTC
65c54e2d703409c726b830b24678bd5ad5d8e5c6
3 files changed
+123
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+21
@@ -39,6 +39,27 @@ export type Options = {
39
40
export function printFunction(fn: HIRFunction): string {
41
const output = [];
42
+ let definition = "";
43
+ if (fn.id !== null) {
44
+ definition += fn.id;
45
+ }
46
+ if (fn.params.length !== 0) {
47
+ definition +=
48
+ "(" +
49
+ fn.params
50
+ .map((param) => {
51
+ if (param.kind === "Identifier") {
52
+ return printPlace(param);
53
+ } else {
54
+ return `...${printPlace(param.place)}`;
55
+ }
56
+ })
57
+ .join(", ") +
58
+ ")";
59
+ }
60
+ if (definition.length !== 0) {
61
+ output.push(definition);
62
+ }
63
output.push(printHIR(fn.body));
64
return output.join("\n");
65
}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-missing-memoization-unmodified-array.expect.md
new
+80
@@ -0,0 +1,80 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useEffect, useState } from "react";
6
+
7
+function Component(props) {
8
+ const x = [props.value];
9
+ useEffect(() => {}, []);
10
+ const onClick = () => {
11
+ console.log(x.length);
12
+ };
13
+ return (
14
+ <div onClick={onClick}>
15
+ {x.map((item) => {
16
+ return <span key={item}>{item}</span>;
17
+ })}
18
+ </div>
19
+ );
20
+}
21
+
22
+export const FIXTURE_ENTRYPOINT = {
23
+ fn: Component,
24
+ params: [{ value: 42 }],
25
+ isComponent: true,
26
+};
27
+
28
+```
29
+
30
+## Code
31
+
32
+```javascript
33
+import {
34
+ useEffect,
35
+ useState,
36
+ unstable_useMemoCache as useMemoCache,
37
+} from "react";
38
+
39
+function Component(props) {
40
+ const $ = useMemoCache(5);
41
+ const x = [props.value];
42
+ let t0;
43
+ let t1;
44
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
45
+ t0 = () => {};
46
+ t1 = [];
47
+ $[0] = t0;
48
+ $[1] = t1;
49
+ } else {
50
+ t0 = $[0];
51
+ t1 = $[1];
52
+ }
53
+ useEffect(t0, t1);
54
+ const onClick = () => {
55
+ console.log(x.length);
56
+ };
57
+
58
+ const t2 = x.map((item) => <span key={item}>{item}</span>);
59
+ let t3;
60
+ if ($[2] !== onClick || $[3] !== t2) {
61
+ t3 = <div onClick={onClick}>{t2}</div>;
62
+ $[2] = onClick;
63
+ $[3] = t2;
64
+ $[4] = t3;
65
+ } else {
66
+ t3 = $[4];
67
+ }
68
+ return t3;
69
+}
70
+
71
+export const FIXTURE_ENTRYPOINT = {
72
+ fn: Component,
73
+ params: [{ value: 42 }],
74
+ isComponent: true,
75
+};
76
+
77
+```
78
+
79
+### Eval output
80
+(kind: ok) <div><span>42</span></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-missing-memoization-unmodified-array.js
new
+22
@@ -0,0 +1,22 @@
1
+import { useEffect, useState } from "react";
2
+
3
+function Component(props) {
4
+ const x = [props.value];
5
+ useEffect(() => {}, []);
6
+ const onClick = () => {
7
+ console.log(x.length);
8
+ };
9
+ return (
10
+ <div onClick={onClick}>
11
+ {x.map((item) => {
12
+ return <span key={item}>{item}</span>;
13
+ })}
14
+ </div>
15
+ );
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: Component,
20
+ params: [{ value: 42 }],
21
+ isComponent: true,
22
+};