@samitouri / QOS-React-2 / commits / ad57e661f4

Sprout support for rendering multiple times w different props

RFC. This is a quick sketch of adding support to Sprout to render the same component instance multiple times with different props. This doesn't test memoization (though it forms a basis for testing it, more below), but does allow us to test that the code properly reacts to inputs and doesn't get "stuck" always returning the same output even when inputs change. Possible extensions: - Support calling non-component functions multiple times - Test memoization by having the `toJSON()` helper track objects it has encountered before, assign each object a unique id, and then emit subsequent references to the same value as the id instead of the printed form of the object. For example if we call a memoized function with the same input twice in a row, today we might get output like: ``` [{a: 1}], [{a: 1}], ``` Which doesn't tell us if the object is equal. Instead we could emit output like: ``` [{a: 1}] #0, #0, ``` Which allows verifying that memoization actually happened. Or we could automate this and just assert that anything structurally equal has to be referentially equal — though there are cases with conditionals that break this.

Joe Savona committed Dec 20, 2023 at 13:52 UTC ad57e661f49f1a340a437a597bef2214c017eabb
8 files changed +133 -25
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md
+61 -17
@@ -3,6 +3,8 @@
3
4 ```javascript
5 // @enableEarlyReturnInReactiveScopes
6 +import { makeArray } from "shared-runtime";
7 +
8 function Component(props) {
9 let x = [];
10 if (props.cond) {
@@ -10,13 +12,29 @@ function Component(props) {
12 // oops no memo!
13 return x;
14 } else {
13 - return foo();
15 + return makeArray(props.b);
16 }
17 }
18
19 export const FIXTURE_ENTRYPOINT = {
20 fn: Component,
19 - params: [{ cond: true, a: 42 }],
21 + params: [],
22 + sequentialRenders: [
23 + // pattern 1
24 + { cond: true, a: 42 },
25 + { cond: true, a: 42 },
26 + // pattern 2
27 + { cond: false, b: 3.14 },
28 + { cond: false, b: 3.14 },
29 + // pattern 1
30 + { cond: true, a: 42 },
31 + // pattern 2
32 + { cond: false, b: 3.14 },
33 + // pattern 1
34 + { cond: true, a: 42 },
35 + // pattern 2
36 + { cond: false, b: 3.14 },
37 + ],
38 };
39
40 ```
@@ -25,45 +43,71 @@ export const FIXTURE_ENTRYPOINT = {
43
44 ```javascript
45 import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEarlyReturnInReactiveScopes
46 +import { makeArray } from "shared-runtime";
47 +
48 function Component(props) {
29 - const $ = useMemoCache(3);
30 - let t29;
49 + const $ = useMemoCache(4);
50 + let t33;
51 if ($[0] !== props) {
32 - t29 = Symbol.for("react.memo_cache_sentinel");
52 + t33 = Symbol.for("react.memo_cache_sentinel");
53 bb8: {
54 const x = [];
55 if (props.cond) {
56 x.push(props.a);
37 - t29 = x;
57 + t33 = x;
58 break bb8;
59 } else {
60 let t0;
41 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
42 - t0 = foo();
43 - $[2] = t0;
61 + if ($[2] !== props.b) {
62 + t0 = makeArray(props.b);
63 + $[2] = props.b;
64 + $[3] = t0;
65 } else {
45 - t0 = $[2];
66 + t0 = $[3];
67 }
47 - t29 = t0;
68 + t33 = t0;
69 break bb8;
70 }
71 }
72 $[0] = props;
52 - $[1] = t29;
73 + $[1] = t33;
74 } else {
54 - t29 = $[1];
75 + t33 = $[1];
76 }
56 - if (t29 !== Symbol.for("react.memo_cache_sentinel")) {
57 - return t29;
77 + if (t33 !== Symbol.for("react.memo_cache_sentinel")) {
78 + return t33;
79 }
80 }
81
82 export const FIXTURE_ENTRYPOINT = {
83 fn: Component,
63 - params: [{ cond: true, a: 42 }],
84 + params: [],
85 + sequentialRenders: [
86 + // pattern 1
87 + { cond: true, a: 42 },
88 + { cond: true, a: 42 },
89 + // pattern 2
90 + { cond: false, b: 3.14 },
91 + { cond: false, b: 3.14 },
92 + // pattern 1
93 + { cond: true, a: 42 },
94 + // pattern 2
95 + { cond: false, b: 3.14 },
96 + // pattern 1
97 + { cond: true, a: 42 },
98 + // pattern 2
99 + { cond: false, b: 3.14 },
100 + ],
101 };
102
103 ```
104
105 ### Eval output
69 -(kind: ok) [42]
\ No newline at end of file
106 +(kind: ok) [42]
107 +[42]
108 +[3.14]
109 +[3.14]
110 +[42]
111 +[3.14]
112 +[42]
113 +[3.14]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.js
+20 -2
@@ -1,4 +1,6 @@
1 // @enableEarlyReturnInReactiveScopes
2 +import { makeArray } from "shared-runtime";
3 +
4 function Component(props) {
5 let x = [];
6 if (props.cond) {
@@ -6,11 +8,27 @@ function Component(props) {
8 // oops no memo!
9 return x;
10 } else {
9 - return foo();
11 + return makeArray(props.b);
12 }
13 }
14
15 export const FIXTURE_ENTRYPOINT = {
16 fn: Component,
15 - params: [{ cond: true, a: 42 }],
17 + params: [],
18 + sequentialRenders: [
19 + // pattern 1
20 + { cond: true, a: 42 },
21 + { cond: true, a: 42 },
22 + // pattern 2
23 + { cond: false, b: 3.14 },
24 + { cond: false, b: 3.14 },
25 + // pattern 1
26 + { cond: true, a: 42 },
27 + // pattern 2
28 + { cond: false, b: 3.14 },
29 + // pattern 1
30 + { cond: true, a: 42 },
31 + // pattern 2
32 + { cond: false, b: 3.14 },
33 + ],
34 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-computed-member-expression.expect.md
+1 -1
@@ -68,7 +68,7 @@ export const FIXTURE_ENTRYPOINT = {
68 (kind: exception) Button is not defined
69 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
70 '\n' +
71 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
71 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:55:26)\n' +
72 '\n' +
73 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
74 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md
+1 -1
@@ -58,7 +58,7 @@ export const FIXTURE_ENTRYPOINT = {
58 (kind: exception) Cannot read properties of null (reading 'push')
59 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
60 '\n' +
61 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
61 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:55:26)\n' +
62 '\n' +
63 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
64 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-locals-named-like-hooks.expect.md
+1 -1
@@ -79,7 +79,7 @@ export const FIXTURE_ENTRYPOINT = {
79 logs: ['The above error occurred in the <div> component:\n' +
80 '\n' +
81 ' at div\n' +
82 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
82 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:55:26)\n' +
83 '\n' +
84 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
85 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-props-named-like-hooks.expect.md
+1 -1
@@ -80,7 +80,7 @@ export const FIXTURE_ENTRYPOINT = {
80 (kind: exception) Cannot read properties of undefined (reading 'useProperty')
81 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
82 '\n' +
83 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
83 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:55:26)\n' +
84 '\n' +
85 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
86 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-throw.expect.md
+1 -1
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
37 (kind: exception) undefined
38 logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
39 '\n' +
40 - ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
40 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:55:26)\n' +
41 '\n' +
42 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
43 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/sprout/src/runner-evaluator.ts
+47 -1
@@ -45,7 +45,14 @@ export type EvaluatorResult = {
45 const EntrypointSchema = z.strictObject({
46 fn: z.union([z.function(), z.object({})]),
47 params: z.array(z.any()),
48 +
49 + // DEPRECATED, unused
50 isComponent: z.optional(z.boolean()),
51 +
52 + // if enabled, the `fn` is assumed to be a component and this is assumed
53 + // to be an array of props. the component is mounted once and rendered
54 + // once per set of props in this array.
55 + sequentialRenders: z.optional(z.nullable(z.array(z.any()))).default(null),
56 });
57 const ExportSchema = z.object({
58 FIXTURE_ENTRYPOINT: EntrypointSchema,
@@ -62,6 +69,35 @@ function WrapperTestComponent(props: { fn: any; params: Array<any> }) {
69 return toJSON(result);
70 }
71 }
72 +
73 +function renderComponentSequentiallyForEachProps(
74 + fn: any,
75 + sequentialRenders: Array<any>
76 +): string {
77 + if (sequentialRenders.length === 0) {
78 + throw new Error(
79 + "Expected at least one set of props when using `sequentialRenders`"
80 + );
81 + }
82 + const initialProps = sequentialRenders[0]!;
83 + const results = [];
84 + const { rerender, container } = render(
85 + React.createElement(WrapperTestComponent, { fn, params: [initialProps] })
86 + );
87 + results.push(container.innerHTML);
88 +
89 + for (let i = 1; i < sequentialRenders.length; i++) {
90 + rerender(
91 + React.createElement(WrapperTestComponent, {
92 + fn,
93 + params: [sequentialRenders[i]],
94 + })
95 + );
96 + results.push(container.innerHTML);
97 + }
98 + return results.join("\n");
99 +}
100 +
101 type FixtureEvaluatorResult = Omit<EvaluatorResult, "logs">;
102 (globalThis as any).evaluateFixtureExport = function (
103 exports: unknown
@@ -78,7 +114,17 @@ type FixtureEvaluatorResult = Omit<EvaluatorResult, "logs">;
114 };
115 }
116 const entrypoint = parsedExportResult.data.FIXTURE_ENTRYPOINT;
81 - if (typeof entrypoint.fn === "object") {
117 + if (entrypoint.sequentialRenders !== null) {
118 + const result = renderComponentSequentiallyForEachProps(
119 + entrypoint.fn,
120 + entrypoint.sequentialRenders
121 + );
122 +
123 + return {
124 + kind: "ok",
125 + value: result ?? "null",
126 + };
127 + } else if (typeof entrypoint.fn === "object") {
128 // Try to run fixture as a react component. This is necessary because not
129 // all components are functions (some are ForwardRef or Memo objects).
130 const result = render(