@samitouri / QOS-React / commits / 9d795d3808

[compiler][bugfix] expand StoreContext to const / let / function variants (#32747)

```js function Component() { useEffect(() => { let hasCleanedUp = false; document.addEventListener(..., () => hasCleanedUp ? foo() : bar()); // effect return values shouldn't be typed as frozen return () => { hasCleanedUp = true; } }; } ``` ### Problem `PruneHoistedContexts` currently strips hoisted declarations and rewrites the first `StoreContext` reassignment to a declaration. For example, in the following example, instruction 0 is removed while a synthetic `DeclareContext let` is inserted before instruction 1. ```js // source const cb = () => x; // reference that causes x to be hoisted let x = 4; x = 5; // React Compiler IR [0] DeclareContext HoistedLet 'x' ... [1] StoreContext reassign 'x' = 4 [2] StoreContext reassign 'x' = 5 ``` Currently, we don't account for `DeclareContext let`. As a result, we're rewriting to insert duplicate declarations. ```js // source const cb = () => x; // reference that causes x to be hoisted let x; x = 5; // React Compiler IR [0] DeclareContext HoistedLet 'x' ... [1] DeclareContext Let 'x' [2] StoreContext reassign 'x' = 5 ``` ### Solution Instead of always lowering context variables to a DeclareContext followed by a StoreContext reassign, we can keep `kind: 'Const' | 'Let' | 'Reassign' | etc` on StoreContext. Pros: - retain more information in HIR, so we can codegen easily `const` and `let` context variable declarations back - pruning hoisted `DeclareContext` instructions is simple. Cons: - passes are more verbose as we need to check for both `DeclareContext` and `StoreContext` declarations ~(note: also see alternative implementation in https://github.com/facebook/react/pull/32745)~ ### Testing Context variables are tricky. I synced and diffed changes in a large meta codebase and feel pretty confident about landing this. About 0.01% of compiled files changed. Among these changes, ~25% were [direct bugfixes](https://www.internalfb.com/phabricator/paste/view/P1800029094). The [other changes](https://www.internalfb.com/phabricator/paste/view/P1800028575) were primarily due to changed (corrected) mutable ranges from https://github.com/facebook/react/pull/33047. I tried to represent most interesting changes in new test fixtures `

mofeiZ committed Apr 30, 2025 at 17:18 UTC 9d795d3808f3202b36740a7a8eb60567bd7f6d90
36 files changed +916 -232
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+31 -22
@@ -3609,31 +3609,40 @@ function lowerAssignment(
3609
3610 let temporary;
3611 if (builder.isContextIdentifier(lvalue)) {
3612 - if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
3613 - if (kind === InstructionKind.Const) {
3614 - builder.errors.push({
3615 - reason: `Expected \`const\` declaration not to be reassigned`,
3616 - severity: ErrorSeverity.InvalidJS,
3617 - loc: lvalue.node.loc ?? null,
3618 - suggestions: null,
3619 - });
3620 - }
3621 - lowerValueToTemporary(builder, {
3622 - kind: 'DeclareContext',
3623 - lvalue: {
3624 - kind: InstructionKind.Let,
3625 - place: {...place},
3626 - },
3627 - loc: place.loc,
3612 + if (kind === InstructionKind.Const && !isHoistedIdentifier) {
3613 + builder.errors.push({
3614 + reason: `Expected \`const\` declaration not to be reassigned`,
3615 + severity: ErrorSeverity.InvalidJS,
3616 + loc: lvalue.node.loc ?? null,
3617 + suggestions: null,
3618 });
3619 }
3620
3631 - temporary = lowerValueToTemporary(builder, {
3632 - kind: 'StoreContext',
3633 - lvalue: {place: {...place}, kind: InstructionKind.Reassign},
3634 - value,
3635 - loc,
3636 - });
3621 + if (
3622 + kind !== InstructionKind.Const &&
3623 + kind !== InstructionKind.Reassign &&
3624 + kind !== InstructionKind.Let &&
3625 + kind !== InstructionKind.Function
3626 + ) {
3627 + builder.errors.push({
3628 + reason: `Unexpected context variable kind`,
3629 + severity: ErrorSeverity.InvalidJS,
3630 + loc: lvalue.node.loc ?? null,
3631 + suggestions: null,
3632 + });
3633 + temporary = lowerValueToTemporary(builder, {
3634 + kind: 'UnsupportedNode',
3635 + node: lvalueNode,
3636 + loc: lvalueNode.loc ?? GeneratedSource,
3637 + });
3638 + } else {
3639 + temporary = lowerValueToTemporary(builder, {
3640 + kind: 'StoreContext',
3641 + lvalue: {place: {...place}, kind},
3642 + value,
3643 + loc,
3644 + });
3645 + }
3646 } else {
3647 const typeAnnotation = lvalue.get('typeAnnotation');
3648 let type: t.FlowType | t.TSType | null;
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+34 -1
@@ -746,6 +746,27 @@ export enum InstructionKind {
746 Function = 'Function',
747 }
748
749 +export function convertHoistedLValueKind(
750 + kind: InstructionKind,
751 +): InstructionKind | null {
752 + switch (kind) {
753 + case InstructionKind.HoistedLet:
754 + return InstructionKind.Let;
755 + case InstructionKind.HoistedConst:
756 + return InstructionKind.Const;
757 + case InstructionKind.HoistedFunction:
758 + return InstructionKind.Function;
759 + case InstructionKind.Let:
760 + case InstructionKind.Const:
761 + case InstructionKind.Function:
762 + case InstructionKind.Reassign:
763 + case InstructionKind.Catch:
764 + return null;
765 + default:
766 + assertExhaustive(kind, 'Unexpected lvalue kind');
767 + }
768 +}
769 +
770 function _staticInvariantInstructionValueHasLocation(
771 value: InstructionValue,
772 ): SourceLocation {
@@ -880,8 +901,20 @@ export type InstructionValue =
901 | StoreLocal
902 | {
903 kind: 'StoreContext';
904 + /**
905 + * StoreContext kinds:
906 + * Reassign: context variable reassignment in source
907 + * Const: const declaration + assignment in source
908 + * ('const' context vars are ones whose declarations are hoisted)
909 + * Let: let declaration + assignment in source
910 + * Function: function declaration in source (similar to `const`)
911 + */
912 lvalue: {
884 - kind: InstructionKind.Reassign;
913 + kind:
914 + | InstructionKind.Reassign
915 + | InstructionKind.Const
916 + | InstructionKind.Let
917 + | InstructionKind.Function;
918 place: Place;
919 };
920 value: Place;
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts
+50 -20
@@ -30,6 +30,7 @@ import {
30 FunctionExpression,
31 ObjectMethod,
32 PropertyLiteral,
33 + convertHoistedLValueKind,
34 } from './HIR';
35 import {
36 collectHoistablePropertyLoads,
@@ -246,12 +247,18 @@ function isLoadContextMutable(
247 id: InstructionId,
248 ): instrValue is LoadContext {
249 if (instrValue.kind === 'LoadContext') {
249 - CompilerError.invariant(instrValue.place.identifier.scope != null, {
250 - reason:
251 - '[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
252 - loc: instrValue.loc,
253 - });
254 - return id >= instrValue.place.identifier.scope.range.end;
250 + /**
251 + * Not all context variables currently have scopes due to limitations of
252 + * mutability analysis for function expressions.
253 + *
254 + * Currently, many function expressions references are inferred to be
255 + * 'Read' | 'Freeze' effects which don't replay mutable effects of captured
256 + * context.
257 + */
258 + return (
259 + instrValue.place.identifier.scope != null &&
260 + id >= instrValue.place.identifier.scope.range.end
261 + );
262 }
263 return false;
264 }
@@ -471,6 +478,9 @@ export class DependencyCollectionContext {
478 }
479 this.#reassignments.set(identifier, decl);
480 }
481 + hasDeclared(identifier: Identifier): boolean {
482 + return this.#declarations.has(identifier.declarationId);
483 + }
484
485 // Checks if identifier is a valid dependency in the current scope
486 #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
@@ -672,21 +682,21 @@ export function handleInstruction(
682 });
683 } else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
684 /*
675 - * Some variables may be declared and never initialized. We need
676 - * to retain (and hoist) these declarations if they are included
677 - * in a reactive scope. One approach is to simply add all `DeclareLocal`s
678 - * as scope declarations.
685 + * Some variables may be declared and never initialized. We need to retain
686 + * (and hoist) these declarations if they are included in a reactive scope.
687 + * One approach is to simply add all `DeclareLocal`s as scope declarations.
688 + *
689 + * Context variables with hoisted declarations only become live after their
690 + * first assignment. We only declare real DeclareLocal / DeclareContext
691 + * instructions (not hoisted ones) to avoid generating dependencies on
692 + * hoisted declarations.
693 */
680 -
681 - /*
682 - * We add context variable declarations here, not at `StoreContext`, since
683 - * context Store / Loads are modeled as reads and mutates to the underlying
684 - * variable reference (instead of through intermediate / inlined temporaries)
685 - */
686 - context.declare(value.lvalue.place.identifier, {
687 - id,
688 - scope: context.currentScope,
689 - });
694 + if (convertHoistedLValueKind(value.lvalue.kind) === null) {
695 + context.declare(value.lvalue.place.identifier, {
696 + id,
697 + scope: context.currentScope,
698 + });
699 + }
700 } else if (value.kind === 'Destructure') {
701 context.visitOperand(value.value);
702 for (const place of eachPatternOperand(value.lvalue.pattern)) {
@@ -698,6 +708,26 @@ export function handleInstruction(
708 scope: context.currentScope,
709 });
710 }
711 + } else if (value.kind === 'StoreContext') {
712 + /**
713 + * Some StoreContext variables have hoisted declarations. If we're storing
714 + * to a context variable that hasn't yet been declared, the StoreContext is
715 + * the declaration.
716 + * (see corresponding logic in PruneHoistedContext)
717 + */
718 + if (
719 + !context.hasDeclared(value.lvalue.place.identifier) ||
720 + value.lvalue.kind !== InstructionKind.Reassign
721 + ) {
722 + context.declare(value.lvalue.place.identifier, {
723 + id,
724 + scope: context.currentScope,
725 + });
726 + }
727 +
728 + for (const operand of eachInstructionValueOperand(value)) {
729 + context.visitOperand(operand);
730 + }
731 } else {
732 for (const operand of eachInstructionValueOperand(value)) {
733 context.visitOperand(operand);
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts
+8 -2
@@ -176,9 +176,15 @@ export function inferMutableLifetimes(
176 if (
177 instr.value.kind === 'DeclareContext' ||
178 (instr.value.kind === 'StoreContext' &&
179 - instr.value.lvalue.kind !== InstructionKind.Reassign)
179 + instr.value.lvalue.kind !== InstructionKind.Reassign &&
180 + !contextVariableDeclarationInstructions.has(
181 + instr.value.lvalue.place.identifier,
182 + ))
183 ) {
181 - // Save declarations of context variables
184 + /**
185 + * Save declarations of context variables if they hasn't already been
186 + * declared (due to hoisted declarations).
187 + */
188 contextVariableDeclarationInstructions.set(
189 instr.value.lvalue.place.identifier,
190 instr.id,
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+15 -2
@@ -407,9 +407,14 @@ class InferenceState {
407
408 freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
409 for (const value of values) {
410 - if (value.kind === 'DeclareContext') {
410 + if (
411 + value.kind === 'DeclareContext' ||
412 + (value.kind === 'StoreContext' &&
413 + (value.lvalue.kind === InstructionKind.Let ||
414 + value.lvalue.kind === InstructionKind.Const))
415 + ) {
416 /**
412 - * Avoid freezing hoisted context declarations
417 + * Avoid freezing context variable declarations, hoisted or otherwise
418 * function Component() {
419 * const cb = useBar(() => foo(2)); // produces a hoisted context declaration
420 * const foo = useFoo(); // reassigns to the context variable
@@ -1606,6 +1611,14 @@ function inferBlock(
1611 );
1612
1613 const lvalue = instr.lvalue;
1614 + if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
1615 + state.initialize(instrValue, {
1616 + kind: ValueKind.Mutable,
1617 + reason: new Set([ValueReason.Other]),
1618 + context: new Set(),
1619 + });
1620 + state.define(instrValue.lvalue.place, instrValue);
1621 + }
1622 state.alias(lvalue, instrValue.value);
1623 lvalue.effect = Effect.Store;
1624 continuation = {kind: 'funeffects'};
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+16
@@ -1000,6 +1000,14 @@ function codegenTerminal(
1000 lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
1001 break;
1002 }
1003 + case 'StoreContext': {
1004 + CompilerError.throwTodo({
1005 + reason: 'Support non-trivial for..in inits',
1006 + description: null,
1007 + loc: terminal.init.loc,
1008 + suggestions: null,
1009 + });
1010 + }
1011 default:
1012 CompilerError.invariant(false, {
1013 reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
@@ -1092,6 +1100,14 @@ function codegenTerminal(
1100 lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
1101 break;
1102 }
1103 + case 'StoreContext': {
1104 + CompilerError.throwTodo({
1105 + reason: 'Support non-trivial for..of inits',
1106 + description: null,
1107 + loc: terminal.init.loc,
1108 + suggestions: null,
1109 + });
1110 + }
1111 default:
1112 CompilerError.invariant(false, {
1113 reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts
+36 -113
@@ -5,14 +5,16 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {CompilerError} from '..';
8 import {
10 - DeclarationId,
9 + convertHoistedLValueKind,
10 + IdentifierId,
11 InstructionKind,
12 ReactiveFunction,
13 ReactiveInstruction,
14 + ReactiveScopeBlock,
15 ReactiveStatement,
16 } from '../HIR';
17 +import {empty, Stack} from '../Utils/Stack';
18 import {
19 ReactiveFunctionTransform,
20 Transformed,
@@ -24,133 +26,54 @@ import {
26 * original instruction kind.
27 */
28 export function pruneHoistedContexts(fn: ReactiveFunction): void {
27 - const hoistedIdentifiers: HoistedIdentifiers = new Map();
28 - visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
29 + visitReactiveFunction(fn, new Visitor(), {
30 + activeScopes: empty(),
31 + });
32 }
33
31 -const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
32 - 'REWRITTEN_HOISTED_CONST',
33 -);
34 -const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
34 +type VisitorState = {
35 + activeScopes: Stack<Set<IdentifierId>>;
36 +};
37
36 -type HoistedIdentifiers = Map<
37 - DeclarationId,
38 - | InstructionKind
39 - | typeof REWRITTEN_HOISTED_CONST
40 - | typeof REWRITTEN_HOISTED_LET
41 ->;
42 -
43 -class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
38 +class Visitor extends ReactiveFunctionTransform<VisitorState> {
39 + override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
40 + state.activeScopes = state.activeScopes.push(
41 + new Set(scope.scope.declarations.keys()),
42 + );
43 + this.traverseScope(scope, state);
44 + state.activeScopes.pop();
45 + }
46 override transformInstruction(
47 instruction: ReactiveInstruction,
46 - state: HoistedIdentifiers,
48 + state: VisitorState,
49 ): Transformed<ReactiveStatement> {
50 this.visitInstruction(instruction, state);
51
52 /**
53 * Remove hoisted declarations to preserve TDZ
54 */
53 - if (
54 - instruction.value.kind === 'DeclareContext' &&
55 - instruction.value.lvalue.kind === 'HoistedConst'
56 - ) {
57 - state.set(
58 - instruction.value.lvalue.place.identifier.declarationId,
59 - InstructionKind.Const,
60 - );
61 - return {kind: 'remove'};
62 - }
63 -
64 - if (
65 - instruction.value.kind === 'DeclareContext' &&
66 - instruction.value.lvalue.kind === 'HoistedLet'
67 - ) {
68 - state.set(
69 - instruction.value.lvalue.place.identifier.declarationId,
70 - InstructionKind.Let,
55 + if (instruction.value.kind === 'DeclareContext') {
56 + const maybeNonHoisted = convertHoistedLValueKind(
57 + instruction.value.lvalue.kind,
58 );
72 - return {kind: 'remove'};
59 + if (maybeNonHoisted != null) {
60 + return {kind: 'remove'};
61 + }
62 }
74 -
63 if (
76 - instruction.value.kind === 'DeclareContext' &&
77 - instruction.value.lvalue.kind === 'HoistedFunction'
64 + instruction.value.kind === 'StoreContext' &&
65 + instruction.value.lvalue.kind !== InstructionKind.Reassign
66 ) {
79 - state.set(
80 - instruction.value.lvalue.place.identifier.declarationId,
81 - InstructionKind.Function,
82 - );
83 - return {kind: 'remove'};
84 - }
85 -
86 - if (instruction.value.kind === 'StoreContext') {
87 - const kind = state.get(
88 - instruction.value.lvalue.place.identifier.declarationId,
67 + /**
68 + * Rewrite StoreContexts let/const/functions that will be pre-declared in
69 + * codegen to reassignments.
70 + */
71 + const lvalueId = instruction.value.lvalue.place.identifier.id;
72 + const isDeclaredByScope = state.activeScopes.find(scope =>
73 + scope.has(lvalueId),
74 );
90 - if (kind != null) {
91 - CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
92 - reason: 'Expected exactly one store to a hoisted const variable',
93 - loc: instruction.loc,
94 - });
95 - if (
96 - kind === InstructionKind.Const ||
97 - kind === InstructionKind.Function
98 - ) {
99 - state.set(
100 - instruction.value.lvalue.place.identifier.declarationId,
101 - REWRITTEN_HOISTED_CONST,
102 - );
103 - return {
104 - kind: 'replace',
105 - value: {
106 - kind: 'instruction',
107 - instruction: {
108 - ...instruction,
109 - value: {
110 - ...instruction.value,
111 - lvalue: {
112 - ...instruction.value.lvalue,
113 - kind,
114 - },
115 - type: null,
116 - kind: 'StoreLocal',
117 - },
118 - },
119 - },
120 - };
121 - } else if (kind !== REWRITTEN_HOISTED_LET) {
122 - /**
123 - * Context variables declared with let may have reassignments. Only
124 - * insert a `DeclareContext` for the first encountered `StoreContext`
125 - * instruction.
126 - */
127 - state.set(
128 - instruction.value.lvalue.place.identifier.declarationId,
129 - REWRITTEN_HOISTED_LET,
130 - );
131 - return {
132 - kind: 'replace-many',
133 - value: [
134 - {
135 - kind: 'instruction',
136 - instruction: {
137 - id: instruction.id,
138 - lvalue: null,
139 - value: {
140 - kind: 'DeclareContext',
141 - lvalue: {
142 - kind: InstructionKind.Let,
143 - place: {...instruction.value.lvalue.place},
144 - },
145 - loc: instruction.value.loc,
146 - },
147 - loc: instruction.loc,
148 - },
149 - },
150 - {kind: 'instruction', instruction},
151 - ],
152 - };
153 - }
75 + if (isDeclaredByScope) {
76 + instruction.value.lvalue.kind = InstructionKind.Reassign;
77 }
78 }
79
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md
+1 -2
@@ -34,8 +34,7 @@ function bar(a, b) {
34 if ($[0] !== a || $[1] !== b) {
35 const x = [a, b];
36 y = {};
37 - let t;
38 - t = {};
37 + let t = {};
38
39 y = x[0][1];
40 t = x[1][0];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md
+1 -2
@@ -35,8 +35,7 @@ function bar(a, b) {
35 if ($[0] !== a || $[1] !== b) {
36 const x = [a, b];
37 y = {};
38 - let t;
39 - t = {};
38 + let t = {};
39 const f0 = function () {
40 y = x[0][1];
41 t = x[1][0];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md
+1 -2
@@ -33,8 +33,7 @@ function useTest() {
33 const $ = _c(1);
34 let t0;
35 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36 - let w;
37 - w = {};
36 + let w = {};
37
38 const t1 = (w = 42);
39 const t2 = w;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-outside-of-lambda.expect.md
+1 -2
@@ -30,8 +30,7 @@ function Component(props) {
30 const $ = _c(1);
31 let t0;
32 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33 - let x;
34 - x = null;
33 + let x = null;
34 const callback = () => {
35 console.log(x);
36 };
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md
+39 -28
@@ -2,13 +2,22 @@
2 ## Input
3
4 ```javascript
5 +import {Stringify, useIdentity} from 'shared-runtime';
6 +
7 function Component() {
6 - const data = useData();
8 + const data = useIdentity(
9 + new Map([
10 + [0, 'value0'],
11 + [1, 'value1'],
12 + ])
13 + );
14 const items = [];
15 // NOTE: `i` is a context variable because it's reassigned and also referenced
16 // within a closure, the `onClick` handler of each item
17 for (let i = MIN; i <= MAX; i += INCREMENT) {
11 - items.push(<div key={i} onClick={() => data.set(i)} />);
18 + items.push(
19 + <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
20 + );
21 }
22 return <>{items}</>;
23 }
@@ -17,10 +26,6 @@ const MIN = 0;
26 const MAX = 3;
27 const INCREMENT = 1;
28
20 -function useData() {
21 - return new Map();
22 -}
23 -
29 export const FIXTURE_ENTRYPOINT = {
30 params: [],
31 fn: Component,
@@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = {
37
38 ```javascript
39 import { c as _c } from "react/compiler-runtime";
40 +import { Stringify, useIdentity } from "shared-runtime";
41 +
42 function Component() {
36 - const $ = _c(2);
37 - const data = useData();
43 + const $ = _c(3);
44 let t0;
39 - if ($[0] !== data) {
45 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
46 + t0 = new Map([
47 + [0, "value0"],
48 + [1, "value1"],
49 + ]);
50 + $[0] = t0;
51 + } else {
52 + t0 = $[0];
53 + }
54 + const data = useIdentity(t0);
55 + let t1;
56 + if ($[1] !== data) {
57 const items = [];
58 for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
42 - items.push(<div key={i} onClick={() => data.set(i)} />);
59 + items.push(
60 + <Stringify
61 + key={i}
62 + onClick={() => data.get(i)}
63 + shouldInvokeFns={true}
64 + />,
65 + );
66 }
67
45 - t0 = <>{items}</>;
46 - $[0] = data;
47 - $[1] = t0;
68 + t1 = <>{items}</>;
69 + $[1] = data;
70 + $[2] = t1;
71 } else {
49 - t0 = $[1];
72 + t1 = $[2];
73 }
51 - return t0;
74 + return t1;
75 }
76
77 const MIN = 0;
78 const MAX = 3;
79 const INCREMENT = 1;
80
58 -function useData() {
59 - const $ = _c(1);
60 - let t0;
61 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
62 - t0 = new Map();
63 - $[0] = t0;
64 - } else {
65 - t0 = $[0];
66 - }
67 - return t0;
68 -}
69 -
81 export const FIXTURE_ENTRYPOINT = {
82 params: [],
83 fn: Component,
@@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = {
86 ```
87
88 ### Eval output
78 -(kind: ok) <div></div><div></div><div></div><div></div>
\ No newline at end of file
89 +(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js
+11 -6
@@ -1,10 +1,19 @@
1 +import {Stringify, useIdentity} from 'shared-runtime';
2 +
3 function Component() {
2 - const data = useData();
4 + const data = useIdentity(
5 + new Map([
6 + [0, 'value0'],
7 + [1, 'value1'],
8 + ])
9 + );
10 const items = [];
11 // NOTE: `i` is a context variable because it's reassigned and also referenced
12 // within a closure, the `onClick` handler of each item
13 for (let i = MIN; i <= MAX; i += INCREMENT) {
7 - items.push(<div key={i} onClick={() => data.set(i)} />);
14 + items.push(
15 + <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
16 + );
17 }
18 return <>{items}</>;
19 }
@@ -13,10 +22,6 @@ const MIN = 0;
22 const MAX = 3;
23 const INCREMENT = 1;
24
16 -function useData() {
17 - return new Map();
18 -}
19 -
25 export const FIXTURE_ENTRYPOINT = {
26 params: [],
27 fn: Component,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.expect.md new
+82
@@ -0,0 +1,82 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {CONST_TRUE, useIdentity} from 'shared-runtime';
6 +
7 +const hidden = CONST_TRUE;
8 +function useFoo() {
9 + const makeCb = useIdentity(() => {
10 + const logIntervalId = () => {
11 + log(intervalId);
12 + };
13 +
14 + let intervalId;
15 + if (!hidden) {
16 + intervalId = 2;
17 + }
18 + return () => {
19 + logIntervalId();
20 + };
21 + });
22 +
23 + return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
24 +}
25 +
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: useFoo,
28 + params: [],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { c as _c } from "react/compiler-runtime";
37 +import { CONST_TRUE, useIdentity } from "shared-runtime";
38 +
39 +const hidden = CONST_TRUE;
40 +function useFoo() {
41 + const $ = _c(4);
42 + const makeCb = useIdentity(_temp);
43 + let t0;
44 + if ($[0] !== makeCb) {
45 + t0 = makeCb();
46 + $[0] = makeCb;
47 + $[1] = t0;
48 + } else {
49 + t0 = $[1];
50 + }
51 + let t1;
52 + if ($[2] !== t0) {
53 + t1 = <Stringify fn={t0} shouldInvokeFns={true} />;
54 + $[2] = t0;
55 + $[3] = t1;
56 + } else {
57 + t1 = $[3];
58 + }
59 + return t1;
60 +}
61 +function _temp() {
62 + const logIntervalId = () => {
63 + log(intervalId);
64 + };
65 + let intervalId;
66 + if (!hidden) {
67 + intervalId = 2;
68 + }
69 + return () => {
70 + logIntervalId();
71 + };
72 +}
73 +
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: useFoo,
76 + params: [],
77 +};
78 +
79 +```
80 +
81 +### Eval output
82 +(kind: exception) Stringify is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.js new
+25
@@ -0,0 +1,25 @@
1 +import {CONST_TRUE, useIdentity} from 'shared-runtime';
2 +
3 +const hidden = CONST_TRUE;
4 +function useFoo() {
5 + const makeCb = useIdentity(() => {
6 + const logIntervalId = () => {
7 + log(intervalId);
8 + };
9 +
10 + let intervalId;
11 + if (!hidden) {
12 + intervalId = 2;
13 + }
14 + return () => {
15 + logIntervalId();
16 + };
17 + });
18 +
19 + return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [],
25 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-invalid-tdz-let.expect.md
+1 -2
@@ -30,8 +30,7 @@ function Foo() {
30 getX = () => x;
31 console.log(getX());
32
33 - let x;
34 - x = 4;
33 + let x = 4;
34 x = x + 5;
35 $[0] = getX;
36 } else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {CONST_NUMBER1, Stringify} from 'shared-runtime';
6 +
7 +function useHook({cond}) {
8 + 'use memo';
9 + const getX = () => x;
10 +
11 + let x;
12 + if (cond) {
13 + x = CONST_NUMBER1;
14 + }
15 + return <Stringify getX={getX} shouldInvokeFns={true} />;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: () => {},
20 + params: [{cond: true}],
21 + sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime";
30 +import { CONST_NUMBER1, Stringify } from "shared-runtime";
31 +
32 +function useHook(t0) {
33 + "use memo";
34 + const $ = _c(2);
35 + const { cond } = t0;
36 + let t1;
37 + if ($[0] !== cond) {
38 + const getX = () => x;
39 +
40 + let x;
41 + if (cond) {
42 + x = CONST_NUMBER1;
43 + }
44 +
45 + t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
46 + $[0] = cond;
47 + $[1] = t1;
48 + } else {
49 + t1 = $[1];
50 + }
51 + return t1;
52 +}
53 +
54 +export const FIXTURE_ENTRYPOINT = {
55 + fn: () => {},
56 + params: [{ cond: true }],
57 + sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
58 +};
59 +
60 +```
61 +
62 +### Eval output
63 +(kind: ok)
64 +
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.js new
+18
@@ -0,0 +1,18 @@
1 +import {CONST_NUMBER1, Stringify} from 'shared-runtime';
2 +
3 +function useHook({cond}) {
4 + 'use memo';
5 + const getX = () => x;
6 +
7 + let x;
8 + if (cond) {
9 + x = CONST_NUMBER1;
10 + }
11 + return <Stringify getX={getX} shouldInvokeFns={true} />;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: () => {},
16 + params: [{cond: true}],
17 + sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration-2.expect.md
+1 -2
@@ -36,8 +36,7 @@ function hoisting(cond) {
36 items.push(bar());
37 };
38
39 - let bar;
40 - bar = _temp;
39 + let bar = _temp;
40 foo();
41 }
42 $[0] = cond;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration.expect.md
+2 -4
@@ -41,11 +41,9 @@ function hoisting() {
41 return result;
42 };
43
44 - let foo;
45 - foo = () => bar + baz;
44 + let foo = () => bar + baz;
45
47 - let bar;
48 - bar = 3;
46 + let bar = 3;
47 const baz = 2;
48 t0 = qux();
49 $[0] = t0;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-let-declaration.expect.md
+1 -2
@@ -37,8 +37,7 @@ function useHook(t0) {
37 if ($[0] !== cond) {
38 const getX = () => x;
39
40 - let x;
41 - x = CONST_NUMBER0;
40 + let x = CONST_NUMBER0;
41 if (cond) {
42 x = x + CONST_NUMBER1;
43 x;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-twice-let-declaration.expect.md
+1 -2
@@ -38,8 +38,7 @@ function useHook(t0) {
38 if ($[0] !== cond) {
39 const getX = () => x;
40
41 - let x;
42 - x = CONST_NUMBER0;
41 + let x = CONST_NUMBER0;
42 if (cond) {
43 x = x + CONST_NUMBER1;
44 x;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md
+2 -4
@@ -29,10 +29,8 @@ function hoisting() {
29 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 foo = () => bar + baz;
31
32 - let bar;
33 - bar = 3;
34 - let baz;
35 - baz = 2;
32 + let bar = 3;
33 + let baz = 2;
34 $[0] = foo;
35 } else {
36 foo = $[0];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md new
+129
@@ -0,0 +1,129 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, useIdentity} from 'shared-runtime';
6 +
7 +function Component({prop1, prop2}) {
8 + 'use memo';
9 +
10 + const data = useIdentity(
11 + new Map([
12 + [0, 'value0'],
13 + [1, 'value1'],
14 + ])
15 + );
16 + let i = 0;
17 + const items = [];
18 + items.push(
19 + <Stringify
20 + key={i}
21 + onClick={() => data.get(i) + prop1}
22 + shouldInvokeFns={true}
23 + />
24 + );
25 + i = i + 1;
26 + items.push(
27 + <Stringify
28 + key={i}
29 + onClick={() => data.get(i) + prop2}
30 + shouldInvokeFns={true}
31 + />
32 + );
33 + return <>{items}</>;
34 +}
35 +
36 +export const FIXTURE_ENTRYPOINT = {
37 + fn: Component,
38 + params: [{prop1: 'prop1', prop2: 'prop2'}],
39 + sequentialRenders: [
40 + {prop1: 'prop1', prop2: 'prop2'},
41 + {prop1: 'prop1', prop2: 'prop2'},
42 + {prop1: 'changed', prop2: 'prop2'},
43 + ],
44 +};
45 +
46 +```
47 +
48 +## Code
49 +
50 +```javascript
51 +import { c as _c } from "react/compiler-runtime";
52 +import { Stringify, useIdentity } from "shared-runtime";
53 +
54 +function Component(t0) {
55 + "use memo";
56 + const $ = _c(12);
57 + const { prop1, prop2 } = t0;
58 + let t1;
59 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
60 + t1 = new Map([
61 + [0, "value0"],
62 + [1, "value1"],
63 + ]);
64 + $[0] = t1;
65 + } else {
66 + t1 = $[0];
67 + }
68 + const data = useIdentity(t1);
69 + let t2;
70 + if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
71 + let i = 0;
72 + const items = [];
73 + items.push(
74 + <Stringify
75 + key={i}
76 + onClick={() => data.get(i) + prop1}
77 + shouldInvokeFns={true}
78 + />,
79 + );
80 + i = i + 1;
81 +
82 + const t3 = i;
83 + let t4;
84 + if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
85 + t4 = () => data.get(i) + prop2;
86 + $[5] = data;
87 + $[6] = i;
88 + $[7] = prop2;
89 + $[8] = t4;
90 + } else {
91 + t4 = $[8];
92 + }
93 + let t5;
94 + if ($[9] !== t3 || $[10] !== t4) {
95 + t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
96 + $[9] = t3;
97 + $[10] = t4;
98 + $[11] = t5;
99 + } else {
100 + t5 = $[11];
101 + }
102 + items.push(t5);
103 + t2 = <>{items}</>;
104 + $[1] = data;
105 + $[2] = prop1;
106 + $[3] = prop2;
107 + $[4] = t2;
108 + } else {
109 + t2 = $[4];
110 + }
111 + return t2;
112 +}
113 +
114 +export const FIXTURE_ENTRYPOINT = {
115 + fn: Component,
116 + params: [{ prop1: "prop1", prop2: "prop2" }],
117 + sequentialRenders: [
118 + { prop1: "prop1", prop2: "prop2" },
119 + { prop1: "prop1", prop2: "prop2" },
120 + { prop1: "changed", prop2: "prop2" },
121 + ],
122 +};
123 +
124 +```
125 +
126 +### Eval output
127 +(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
128 +<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
129 +<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.js new
+40
@@ -0,0 +1,40 @@
1 +import {Stringify, useIdentity} from 'shared-runtime';
2 +
3 +function Component({prop1, prop2}) {
4 + 'use memo';
5 +
6 + const data = useIdentity(
7 + new Map([
8 + [0, 'value0'],
9 + [1, 'value1'],
10 + ])
11 + );
12 + let i = 0;
13 + const items = [];
14 + items.push(
15 + <Stringify
16 + key={i}
17 + onClick={() => data.get(i) + prop1}
18 + shouldInvokeFns={true}
19 + />
20 + );
21 + i = i + 1;
22 + items.push(
23 + <Stringify
24 + key={i}
25 + onClick={() => data.get(i) + prop2}
26 + shouldInvokeFns={true}
27 + />
28 + );
29 + return <>{items}</>;
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: Component,
34 + params: [{prop1: 'prop1', prop2: 'prop2'}],
35 + sequentialRenders: [
36 + {prop1: 'prop1', prop2: 'prop2'},
37 + {prop1: 'prop1', prop2: 'prop2'},
38 + {prop1: 'changed', prop2: 'prop2'},
39 + ],
40 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-reassign-shadowed-primitive.expect.md
+1 -2
@@ -37,8 +37,7 @@ function Component() {
37 }
38 const x = t0;
39
40 - let x_0;
41 - x_0 = 56;
40 + let x_0 = 56;
41 const fn = function () {
42 x_0 = 42;
43 };
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mutate-captured-arg-separately.expect.md
+1 -2
@@ -33,8 +33,7 @@ function component(a) {
33 m(x);
34 };
35
36 - let x;
37 - x = { a };
36 + let x = { a };
37 m(x);
38 $[0] = a;
39 $[1] = y;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md
+1 -2
@@ -65,8 +65,7 @@ function useBar(t0, cond) {
65 } else {
66 t1 = $[0];
67 }
68 - let x;
69 - x = useIdentity(t1);
68 + let x = useIdentity(t1);
69 if (cond) {
70 x = b;
71 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md
+1 -2
@@ -47,8 +47,7 @@ function Foo(t0) {
47 if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
48 const x = [arr1];
49
50 - let y;
51 - y = [];
50 + let y = [];
51
52 getVal1 = _temp;
53
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md
+1 -2
@@ -47,8 +47,7 @@ function Foo(t0) {
47 if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
48 const x = [arr1];
49
50 - let y;
51 - y = [];
50 + let y = [];
51 let t2;
52 let t3;
53 if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md new
+108
@@ -0,0 +1,108 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useState, useEffect} from 'react';
6 +import {invoke, Stringify} from 'shared-runtime';
7 +
8 +function Content() {
9 + const [announcement, setAnnouncement] = useState('');
10 + const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
11 +
12 + // This was originally passed down as an onClick, but React Compiler's test
13 + // evaluator doesn't yet support events outside of React
14 + useEffect(() => {
15 + if (users.length === 2) {
16 + let removedUserName = '';
17 + setUsers(prevUsers => {
18 + const newUsers = [...prevUsers];
19 + removedUserName = newUsers.at(-1).name;
20 + newUsers.pop();
21 + return newUsers;
22 + });
23 +
24 + setAnnouncement(`Removed user (${removedUserName})`);
25 + }
26 + }, [users]);
27 +
28 + return <Stringify users={users} announcement={announcement} />;
29 +}
30 +
31 +export const FIXTURE_ENTRYPOINT = {
32 + fn: Content,
33 + params: [{}],
34 + sequentialRenders: [{}, {}],
35 +};
36 +
37 +```
38 +
39 +## Code
40 +
41 +```javascript
42 +import { c as _c } from "react/compiler-runtime";
43 +import { useState, useEffect } from "react";
44 +import { invoke, Stringify } from "shared-runtime";
45 +
46 +function Content() {
47 + const $ = _c(8);
48 + const [announcement, setAnnouncement] = useState("");
49 + let t0;
50 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
51 + t0 = [{ name: "John Doe" }, { name: "Jane Doe" }];
52 + $[0] = t0;
53 + } else {
54 + t0 = $[0];
55 + }
56 + const [users, setUsers] = useState(t0);
57 + let t1;
58 + if ($[1] !== users.length) {
59 + t1 = () => {
60 + if (users.length === 2) {
61 + let removedUserName = "";
62 + setUsers((prevUsers) => {
63 + const newUsers = [...prevUsers];
64 + removedUserName = newUsers.at(-1).name;
65 + newUsers.pop();
66 + return newUsers;
67 + });
68 +
69 + setAnnouncement(`Removed user (${removedUserName})`);
70 + }
71 + };
72 + $[1] = users.length;
73 + $[2] = t1;
74 + } else {
75 + t1 = $[2];
76 + }
77 + let t2;
78 + if ($[3] !== users) {
79 + t2 = [users];
80 + $[3] = users;
81 + $[4] = t2;
82 + } else {
83 + t2 = $[4];
84 + }
85 + useEffect(t1, t2);
86 + let t3;
87 + if ($[5] !== announcement || $[6] !== users) {
88 + t3 = <Stringify users={users} announcement={announcement} />;
89 + $[5] = announcement;
90 + $[6] = users;
91 + $[7] = t3;
92 + } else {
93 + t3 = $[7];
94 + }
95 + return t3;
96 +}
97 +
98 +export const FIXTURE_ENTRYPOINT = {
99 + fn: Content,
100 + params: [{}],
101 + sequentialRenders: [{}, {}],
102 +};
103 +
104 +```
105 +
106 +### Eval output
107 +(kind: ok) <div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
108 +<div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.js new
+31
@@ -0,0 +1,31 @@
1 +import {useState, useEffect} from 'react';
2 +import {invoke, Stringify} from 'shared-runtime';
3 +
4 +function Content() {
5 + const [announcement, setAnnouncement] = useState('');
6 + const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
7 +
8 + // This was originally passed down as an onClick, but React Compiler's test
9 + // evaluator doesn't yet support events outside of React
10 + useEffect(() => {
11 + if (users.length === 2) {
12 + let removedUserName = '';
13 + setUsers(prevUsers => {
14 + const newUsers = [...prevUsers];
15 + removedUserName = newUsers.at(-1).name;
16 + newUsers.pop();
17 + return newUsers;
18 + });
19 +
20 + setAnnouncement(`Removed user (${removedUserName})`);
21 + }
22 + }, [users]);
23 +
24 + return <Stringify users={users} announcement={announcement} />;
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: Content,
29 + params: [{}],
30 + sequentialRenders: [{}, {}],
31 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md
+1 -2
@@ -62,8 +62,7 @@ function Foo(t0) {
62 myVar = _temp;
63 };
64
65 - let myVar;
66 - myVar = _temp2;
65 + let myVar = _temp2;
66 useIdentity();
67
68 const fn = fnFactory();
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.expect.md new
+122
@@ -0,0 +1,122 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useEffect, useState} from 'react';
6 +
7 +/**
8 + * Example of a function expression whose return value shouldn't have
9 + * a "freeze" effect on all operands.
10 + *
11 + * This is because the function expression is passed to `useEffect` and
12 + * thus is not a render function. `cleanedUp` is also created within
13 + * the effect and is not a render variable.
14 + */
15 +function Component({prop}) {
16 + const [cleanupCount, setCleanupCount] = useState(0);
17 +
18 + useEffect(() => {
19 + let cleanedUp = false;
20 + setTimeout(() => {
21 + if (!cleanedUp) {
22 + cleanedUp = true;
23 + setCleanupCount(c => c + 1);
24 + }
25 + }, 0);
26 + // This return value should not have freeze effects
27 + // on its operands
28 + return () => {
29 + if (!cleanedUp) {
30 + cleanedUp = true;
31 + setCleanupCount(c => c + 1);
32 + }
33 + };
34 + }, [prop]);
35 + return <div>{cleanupCount}</div>;
36 +}
37 +
38 +export const FIXTURE_ENTRYPOINT = {
39 + fn: Component,
40 + params: [{prop: 5}],
41 + sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
42 +};
43 +
44 +```
45 +
46 +## Code
47 +
48 +```javascript
49 +import { c as _c } from "react/compiler-runtime";
50 +import { useEffect, useState } from "react";
51 +
52 +/**
53 + * Example of a function expression whose return value shouldn't have
54 + * a "freeze" effect on all operands.
55 + *
56 + * This is because the function expression is passed to `useEffect` and
57 + * thus is not a render function. `cleanedUp` is also created within
58 + * the effect and is not a render variable.
59 + */
60 +function Component(t0) {
61 + const $ = _c(5);
62 + const { prop } = t0;
63 + const [cleanupCount, setCleanupCount] = useState(0);
64 + let t1;
65 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
66 + t1 = () => {
67 + let cleanedUp = false;
68 + setTimeout(() => {
69 + if (!cleanedUp) {
70 + cleanedUp = true;
71 + setCleanupCount(_temp);
72 + }
73 + }, 0);
74 + return () => {
75 + if (!cleanedUp) {
76 + cleanedUp = true;
77 + setCleanupCount(_temp2);
78 + }
79 + };
80 + };
81 + $[0] = t1;
82 + } else {
83 + t1 = $[0];
84 + }
85 + let t2;
86 + if ($[1] !== prop) {
87 + t2 = [prop];
88 + $[1] = prop;
89 + $[2] = t2;
90 + } else {
91 + t2 = $[2];
92 + }
93 + useEffect(t1, t2);
94 + let t3;
95 + if ($[3] !== cleanupCount) {
96 + t3 = <div>{cleanupCount}</div>;
97 + $[3] = cleanupCount;
98 + $[4] = t3;
99 + } else {
100 + t3 = $[4];
101 + }
102 + return t3;
103 +}
104 +function _temp2(c_0) {
105 + return c_0 + 1;
106 +}
107 +function _temp(c) {
108 + return c + 1;
109 +}
110 +
111 +export const FIXTURE_ENTRYPOINT = {
112 + fn: Component,
113 + params: [{ prop: 5 }],
114 + sequentialRenders: [{ prop: 5 }, { prop: 5 }, { prop: 6 }],
115 +};
116 +
117 +```
118 +
119 +### Eval output
120 +(kind: ok) <div>0</div>
121 +<div>0</div>
122 +<div>1</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.js new
+38
@@ -0,0 +1,38 @@
1 +import {useEffect, useState} from 'react';
2 +
3 +/**
4 + * Example of a function expression whose return value shouldn't have
5 + * a "freeze" effect on all operands.
6 + *
7 + * This is because the function expression is passed to `useEffect` and
8 + * thus is not a render function. `cleanedUp` is also created within
9 + * the effect and is not a render variable.
10 + */
11 +function Component({prop}) {
12 + const [cleanupCount, setCleanupCount] = useState(0);
13 +
14 + useEffect(() => {
15 + let cleanedUp = false;
16 + setTimeout(() => {
17 + if (!cleanedUp) {
18 + cleanedUp = true;
19 + setCleanupCount(c => c + 1);
20 + }
21 + }, 0);
22 + // This return value should not have freeze effects
23 + // on its operands
24 + return () => {
25 + if (!cleanedUp) {
26 + cleanedUp = true;
27 + setCleanupCount(c => c + 1);
28 + }
29 + };
30 + }, [prop]);
31 + return <div>{cleanupCount}</div>;
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Component,
36 + params: [{prop: 5}],
37 + sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
38 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md
+1 -2
@@ -79,8 +79,7 @@ function Component(props) {
79
80 function Inner(props) {
81 const $ = _c(7);
82 - let input;
83 - input = null;
82 + let input = null;
83 if (props.cond) {
84 input = use(FooContext);
85 }