From 7ee974de927e9bbe10a44441ab49bafd9f5467a2 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes <57368278+jorge-cab@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:44:45 -0800 Subject: [PATCH 1/4] [compiler] Prevent innaccurate derivation recording on FunctionExpressions on no-derived-computation-in-effects (#35173) Summary: The operands of a function expression are the elements passed as context. This means that it doesn't make sense to record mutations for them. The relevant mutations will happen in the function body, so we need to prevent FunctionExpression type instruction from running the logic for effect mutations. This was also causing some values to depend on themselves in some cases triggering an infinite loop. Also added n invariant to prevent this issue Test Plan: Added fixture test --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35173). * #35174 * __->__ #35173 --- ...idateNoDerivedComputationsInEffects_exp.ts | 37 ++++++ ...on-expression-mutation-edge-case.expect.md | 115 ++++++++++++++++++ .../function-expression-mutation-edge-case.js | 32 +++++ 3 files changed, 184 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts index af5927548af..35832b41ac8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts @@ -52,6 +52,8 @@ type ValidationContext = { readonly setStateUsages: Map>; }; +const MAX_FIXPOINT_ITERATIONS = 100; + class DerivationCache { hasChanges: boolean = false; cache: Map = new Map(); @@ -224,6 +226,7 @@ export function validateNoDerivedComputationsInEffects_exp( } let isFirstPass = true; + let iterationCount = 0; do { context.derivationCache.takeSnapshot(); @@ -236,6 +239,19 @@ export function validateNoDerivedComputationsInEffects_exp( context.derivationCache.checkForChanges(); isFirstPass = false; + iterationCount++; + CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, { + reason: + '[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.', + description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`, + details: [ + { + kind: 'error', + loc: fn.loc, + message: `Exceeded ${MAX_FIXPOINT_ITERATIONS} iterations in ValidateNoDerivedComputationsInEffects`, + }, + ], + }); } while (context.derivationCache.snapshot()); for (const [, effect] of effectsCache) { @@ -422,6 +438,14 @@ function recordInstructionDerivations( ); } + if (value.kind === 'FunctionExpression') { + /* + * We don't want to record effect mutations of FunctionExpressions the mutations will happen in the + * function body and we will record them there. + */ + return; + } + for (const operand of eachInstructionOperand(instr)) { switch (operand.effect) { case Effect.Capture: @@ -512,6 +536,19 @@ function buildTreeNode( const namedSiblings: Set = new Set(); for (const childId of sourceMetadata.sourcesIds) { + CompilerError.invariant(childId !== sourceId, { + reason: + 'Unexpected self-reference: a value should not have itself as a source', + description: null, + details: [ + { + kind: 'error', + loc: sourceMetadata.place.loc, + message: null, + }, + ], + }); + const childNodes = buildTreeNode( childId, context, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.expect.md new file mode 100644 index 00000000000..4e0ff4e2394 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.expect.md @@ -0,0 +1,115 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly + +function Component() { + const [foo, setFoo] = useState({}); + const [bar, setBar] = useState(new Set()); + + /* + * isChanged is considered context of the effect's function expression, + * if we don't bail out of effect mutation derivation tracking, isChanged + * will inherit the sources of the effect's function expression. + * + * This is innacurate and with the multiple passes ends up causing an infinite loop. + */ + useEffect(() => { + let isChanged = false; + + const newData = foo.map(val => { + bar.someMethod(val); + isChanged = true; + }); + + if (isChanged) { + setFoo(newData); + } + }, [foo, bar]); + + return ( +
+ {foo}, {bar} +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly + +function Component() { + const $ = _c(9); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = {}; + $[0] = t0; + } else { + t0 = $[0]; + } + const [foo, setFoo] = useState(t0); + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = new Set(); + $[1] = t1; + } else { + t1 = $[1]; + } + const [bar] = useState(t1); + let t2; + let t3; + if ($[2] !== bar || $[3] !== foo) { + t2 = () => { + let isChanged = false; + + const newData = foo.map((val) => { + bar.someMethod(val); + isChanged = true; + }); + + if (isChanged) { + setFoo(newData); + } + }; + + t3 = [foo, bar]; + $[2] = bar; + $[3] = foo; + $[4] = t2; + $[5] = t3; + } else { + t2 = $[4]; + t3 = $[5]; + } + useEffect(t2, t3); + let t4; + if ($[6] !== bar || $[7] !== foo) { + t4 = ( +
+ {foo}, {bar} +
+ ); + $[6] = bar; + $[7] = foo; + $[8] = t4; + } else { + t4 = $[8]; + } + return t4; +} + +``` + +## Logs + +``` +{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [foo, bar]\n\nData Flow Tree:\n└── newData\n ├── foo (State)\n └── bar (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":23,"column":6,"index":663},"end":{"line":23,"column":12,"index":669},"filename":"function-expression-mutation-edge-case.ts","identifierName":"setFoo"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":32,"column":1,"index":762},"filename":"function-expression-mutation-edge-case.ts"},"fnName":"Component","memoSlots":9,"memoBlocks":4,"memoValues":5,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.js new file mode 100644 index 00000000000..ab0bd70f363 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/function-expression-mutation-edge-case.js @@ -0,0 +1,32 @@ +// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly + +function Component() { + const [foo, setFoo] = useState({}); + const [bar, setBar] = useState(new Set()); + + /* + * isChanged is considered context of the effect's function expression, + * if we don't bail out of effect mutation derivation tracking, isChanged + * will inherit the sources of the effect's function expression. + * + * This is innacurate and with the multiple passes ends up causing an infinite loop. + */ + useEffect(() => { + let isChanged = false; + + const newData = foo.map(val => { + bar.someMethod(val); + isChanged = true; + }); + + if (isChanged) { + setFoo(newData); + } + }, [foo, bar]); + + return ( +
+ {foo}, {bar} +
+ ); +} From 7d67591041e3ac47eac3bab6cff209071d4c0c6c Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes <57368278+jorge-cab@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:45:17 -0800 Subject: [PATCH 2/4] [compiler] Remove useState argument constraint. no-derived-computations-in-effects (#35174) Summary: I missed this conditional messing things up for undefined useState() calls. We should be tracking them. I also missed a test that expect an error was not throwing. Test Plan: Update broken test --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35174). * __->__ #35174 * #35173 --- .../Validation/ValidateNoDerivedComputationsInEffects_exp.ts | 2 +- ...estate-derived-from-prop-no-show-in-data-flow-tree.expect.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts index 35832b41ac8..bb59cf1d468 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts @@ -388,7 +388,7 @@ function recordInstructionDerivations( dependencies: deps, }); } - } else if (isUseStateType(lvalue.identifier) && value.args.length > 0) { + } else if (isUseStateType(lvalue.identifier)) { typeOfValue = 'fromState'; context.derivationCache.addDerivationEntry( lvalue, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/usestate-derived-from-prop-no-show-in-data-flow-tree.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/usestate-derived-from-prop-no-show-in-data-flow-tree.expect.md index 602fb9fff3a..87cf7722da3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/usestate-derived-from-prop-no-show-in-data-flow-tree.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/usestate-derived-from-prop-no-show-in-data-flow-tree.expect.md @@ -64,6 +64,7 @@ function Component(t0) { ## Logs ``` +{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [second]\n\nData Flow Tree:\n└── second (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":14,"column":4,"index":443},"end":{"line":14,"column":8,"index":447},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":18,"column":1,"index":500},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` From 4cf770d7e1a52c66401b42c7d135f40b7dc23981 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:02:38 -0800 Subject: [PATCH 3/4] [compiler][poc] Quick experiment with SSR-optimization pass (#35102) Just a quick poc: * Inline useState when the initializer is known to not be a function. The heuristic could be improved but will handle a large number of cases already. * Prune effects * Prune useRef if the ref is unused, by pruning 'ref' props on primitive components. Then DCE does the rest of the work - with a small change to allow `useRef()` calls to be dropped since function calls aren't normally eligible for dropping. * Prune event handlers, by pruning props whose names start w "on" from primitive components. Then DCE removes the functions themselves. Per the fixture, this gets pretty far. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35102). * #35112 * __->__ #35102 --- .../src/Entrypoint/Pipeline.ts | 12 +- .../src/HIR/Environment.ts | 2 + .../src/HIR/HIR.ts | 4 + .../src/Optimization/DeadCodeElimination.ts | 28 +- .../src/Optimization/OptimizeForSSR.ts | 269 ++++++++++++++++++ .../compiler/ssr/optimize-ssr.expect.md | 30 ++ .../fixtures/compiler/ssr/optimize-ssr.js | 12 + ...fer-event-handlers-from-setState.expect.md | 36 +++ .../ssr-infer-event-handlers-from-setState.js | 14 + ...nt-handlers-from-startTransition.expect.md | 40 +++ ...fer-event-handlers-from-startTransition.js | 17 ++ .../ssr/ssr-use-reducer-initializer.expect.md | 42 +++ .../ssr/ssr-use-reducer-initializer.js | 17 ++ .../compiler/ssr/ssr-use-reducer.expect.md | 36 +++ .../fixtures/compiler/ssr/ssr-use-reducer.js | 15 + .../packages/snap/src/SproutTodoFilter.ts | 7 + 16 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Optimization/OptimizeForSSR.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 132507f41a3..c01aceb6e8c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects'; import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp'; import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions'; +import {optimizeForSSR} from '../Optimization/OptimizeForSSR'; import {validateSourceLocations} from '../Validation/ValidateSourceLocations'; export type CompilerPipelineValue = @@ -237,6 +238,11 @@ function runWithEnvironment( } } + if (env.config.enableOptimizeForSSR) { + optimizeForSSR(hir); + log({kind: 'hir', name: 'OptimizeForSSR', value: hir}); + } + // Note: Has to come after infer reference effects because "dead" code may still affect inference deadCodeElimination(hir); log({kind: 'hir', name: 'DeadCodeElimination', value: hir}); @@ -314,8 +320,10 @@ function runWithEnvironment( * if inferred memoization is enabled. This makes all later passes which * transform reactive-scope labeled instructions no-ops. */ - inferReactiveScopeVariables(hir); - log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); + if (!env.config.enableOptimizeForSSR) { + inferReactiveScopeVariables(hir); + log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); + } } const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index b29d65e6aa5..0e3654dccaf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -691,6 +691,8 @@ export const EnvironmentConfigSchema = z.object({ * by React to only execute in response to events, not during render. */ enableInferEventHandlers: z.boolean().default(false), + + enableOptimizeForSSR: z.boolean().default(false), }); export type EnvironmentConfig = z.infer; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 5a19490cb6c..87ca692a95f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1823,6 +1823,10 @@ export function isPrimitiveType(id: Identifier): boolean { return id.type.kind === 'Primitive'; } +export function isPlainObjectType(id: Identifier): boolean { + return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject'; +} + export function isArrayType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray'; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts index 2b752c6dfd2..0dbc8c471be 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts @@ -7,6 +7,8 @@ import { BlockId, + Environment, + getHookKind, HIRFunction, Identifier, IdentifierId, @@ -68,9 +70,14 @@ export function deadCodeElimination(fn: HIRFunction): void { } class State { + env: Environment; named: Set = new Set(); identifiers: Set = new Set(); + constructor(env: Environment) { + this.env = env; + } + // Mark the identifier as being referenced (not dead code) reference(identifier: Identifier): void { this.identifiers.add(identifier.id); @@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State { const hasLoop = hasBackEdge(fn); const reversedBlocks = [...fn.body.blocks.values()].reverse(); - const state = new State(); + const state = new State(fn.env); let size = state.count; do { size = state.count; @@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean { // explicitly retain debugger statements to not break debugging workflows return false; } - case 'Await': case 'CallExpression': + case 'MethodCall': { + if (state.env.config.enableOptimizeForSSR) { + const calleee = + value.kind === 'CallExpression' ? value.callee : value.property; + const hookKind = getHookKind(state.env, calleee.identifier); + switch (hookKind) { + case 'useState': + case 'useReducer': + case 'useRef': { + // unused refs can be removed + return true; + } + } + } + return false; + } + case 'Await': case 'ComputedDelete': case 'ComputedStore': case 'PropertyDelete': - case 'MethodCall': case 'PropertyStore': case 'StoreGlobal': { /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OptimizeForSSR.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OptimizeForSSR.ts new file mode 100644 index 00000000000..3fd7db1fc6d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OptimizeForSSR.ts @@ -0,0 +1,269 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerError} from '..'; +import { + CallExpression, + getHookKind, + HIRFunction, + IdentifierId, + InstructionValue, + isArrayType, + isPlainObjectType, + isPrimitiveType, + isSetStateType, + isStartTransitionType, + LoadLocal, + StoreLocal, +} from '../HIR'; +import { + eachInstructionValueOperand, + eachTerminalOperand, +} from '../HIR/visitors'; +import {retainWhere} from '../Utils/utils'; + +/** + * Optimizes the code for running specifically in an SSR environment. This optimization + * asssumes that setState will not be called during render during initial mount, which + * allows inlining useState/useReducer. + * + * Optimizations: + * - Inline useState/useReducer + * - Remove effects + * - Remove refs where known to be unused during render (eg directly passed to a dom node) + * - Remove event handlers + * + * Note that an earlier pass already inlines useMemo/useCallback + */ +export function optimizeForSSR(fn: HIRFunction): void { + const inlinedState = new Map(); + /** + * First pass identifies useState/useReducer which can be safely inlined. Any use + * of the hook return other than destructuring (with a specific pattern) prevents + * inlining. + * + * Supported cases: + * - `const [state, ] = useState( )` + * - `const [state, ] = useReducer(..., )` + * - `const [state, ] = useReducer[..., , ]` + */ + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {value} = instr; + switch (value.kind) { + case 'Destructure': { + if ( + inlinedState.has(value.value.identifier.id) && + value.lvalue.pattern.kind === 'ArrayPattern' && + value.lvalue.pattern.items.length >= 1 && + value.lvalue.pattern.items[0].kind === 'Identifier' + ) { + // Allow destructuring of inlined states + continue; + } + break; + } + case 'MethodCall': + case 'CallExpression': { + const calleee = + value.kind === 'CallExpression' ? value.callee : value.property; + const hookKind = getHookKind(fn.env, calleee.identifier); + switch (hookKind) { + case 'useReducer': { + if ( + value.args.length === 2 && + value.args[1].kind === 'Identifier' + ) { + const arg = value.args[1]; + const replace: LoadLocal = { + kind: 'LoadLocal', + place: arg, + loc: arg.loc, + }; + inlinedState.set(instr.lvalue.identifier.id, replace); + } else if ( + value.args.length === 3 && + value.args[1].kind === 'Identifier' && + value.args[2].kind === 'Identifier' + ) { + const arg = value.args[1]; + const initializer = value.args[2]; + const replace: CallExpression = { + kind: 'CallExpression', + callee: initializer, + args: [arg], + loc: value.loc, + }; + inlinedState.set(instr.lvalue.identifier.id, replace); + } + break; + } + case 'useState': { + if ( + value.args.length === 1 && + value.args[0].kind === 'Identifier' + ) { + const arg = value.args[0]; + if ( + isPrimitiveType(arg.identifier) || + isPlainObjectType(arg.identifier) || + isArrayType(arg.identifier) + ) { + const replace: LoadLocal = { + kind: 'LoadLocal', + place: arg, + loc: arg.loc, + }; + inlinedState.set(instr.lvalue.identifier.id, replace); + } + } + break; + } + } + } + } + // Any use of useState/useReducer return besides destructuring prevents inlining + if (inlinedState.size !== 0) { + for (const operand of eachInstructionValueOperand(value)) { + inlinedState.delete(operand.identifier.id); + } + } + } + if (inlinedState.size !== 0) { + for (const operand of eachTerminalOperand(block.terminal)) { + inlinedState.delete(operand.identifier.id); + } + } + } + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {value} = instr; + switch (value.kind) { + case 'FunctionExpression': { + if (hasKnownNonRenderCall(value.loweredFunc.func)) { + instr.value = { + kind: 'Primitive', + value: undefined, + loc: value.loc, + }; + } + break; + } + case 'JsxExpression': { + if ( + value.tag.kind === 'BuiltinTag' && + value.tag.name.indexOf('-') === -1 + ) { + const tag = value.tag.name; + retainWhere(value.props, prop => { + return ( + prop.kind === 'JsxSpreadAttribute' || + (!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref') + ); + }); + } + break; + } + case 'Destructure': { + if (inlinedState.has(value.value.identifier.id)) { + // Canonical check is part of determining if state can inline, this is for TS + CompilerError.invariant( + value.lvalue.pattern.kind === 'ArrayPattern' && + value.lvalue.pattern.items.length >= 1 && + value.lvalue.pattern.items[0].kind === 'Identifier', + { + reason: + 'Expected a valid destructuring pattern for inlined state', + description: null, + details: [ + { + kind: 'error', + message: 'Expected a valid destructuring pattern', + loc: value.loc, + }, + ], + }, + ); + const store: StoreLocal = { + kind: 'StoreLocal', + loc: value.loc, + type: null, + lvalue: { + kind: value.lvalue.kind, + place: value.lvalue.pattern.items[0], + }, + value: value.value, + }; + instr.value = store; + } + break; + } + case 'MethodCall': + case 'CallExpression': { + const calleee = + value.kind === 'CallExpression' ? value.callee : value.property; + const hookKind = getHookKind(fn.env, calleee.identifier); + switch (hookKind) { + case 'useEffectEvent': { + if ( + value.args.length === 1 && + value.args[0].kind === 'Identifier' + ) { + const load: LoadLocal = { + kind: 'LoadLocal', + place: value.args[0], + loc: value.loc, + }; + instr.value = load; + } + break; + } + case 'useEffect': + case 'useLayoutEffect': + case 'useInsertionEffect': { + // Drop effects + instr.value = { + kind: 'Primitive', + value: undefined, + loc: value.loc, + }; + break; + } + case 'useReducer': + case 'useState': { + const replace = inlinedState.get(instr.lvalue.identifier.id); + if (replace != null) { + instr.value = replace; + } + break; + } + } + } + } + } + } +} + +function hasKnownNonRenderCall(fn: HIRFunction): boolean { + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'CallExpression' && + (isSetStateType(instr.value.callee.identifier) || + isStartTransitionType(instr.value.callee.identifier)) + ) { + return true; + } + } + } + return false; +} + +const EVENT_HANDLER_PATTERN = /^on[A-Z]/; +function isKnownEventHandler(_tag: string, prop: string): boolean { + return EVENT_HANDLER_PATTERN.test(prop); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md new file mode 100644 index 00000000000..3508aab5358 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +// @enableOptimizeForSSR +function Component() { + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + setState(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} + +``` + +## Code + +```javascript +// @enableOptimizeForSSR +function Component() { + const state = 0; + return ; +} + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.js new file mode 100644 index 00000000000..d9fba0f3903 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.js @@ -0,0 +1,12 @@ +// @enableOptimizeForSSR +function Component() { + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + setState(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md new file mode 100644 index 00000000000..0aeb890c26d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @enableOptimizeForSSR +function Component() { + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + // The known setState call allows us to infer this as an event handler + // and prune it + setState(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} + +``` + +## Code + +```javascript +// @enableOptimizeForSSR +function Component() { + const state = 0; + const ref = useRef(null); + const onChange = undefined; + return ; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.js new file mode 100644 index 00000000000..c67f026c040 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.js @@ -0,0 +1,14 @@ +// @enableOptimizeForSSR +function Component() { + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + // The known setState call allows us to infer this as an event handler + // and prune it + setState(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md new file mode 100644 index 00000000000..53cf10a678a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @enableOptimizeForSSR +function Component() { + const [, startTransition] = useTransition(); + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + // The known startTransition call allows us to infer this as an event handler + // and prune it + startTransition(() => { + setState.call(null, e.target.value); + }); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} + +``` + +## Code + +```javascript +// @enableOptimizeForSSR +function Component() { + useTransition(); + const state = 0; + const ref = useRef(null); + const onChange = undefined; + return ; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.js new file mode 100644 index 00000000000..f6f6f3914dc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.js @@ -0,0 +1,17 @@ +// @enableOptimizeForSSR +function Component() { + const [, startTransition] = useTransition(); + const [state, setState] = useState(0); + const ref = useRef(null); + const onChange = e => { + // The known startTransition call allows us to infer this as an event handler + // and prune it + startTransition(() => { + setState.call(null, e.target.value); + }); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md new file mode 100644 index 00000000000..ead89e12887 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @enableOptimizeForSSR + +import {useReducer} from 'react'; + +const initializer = x => x; + +function Component() { + const [state, dispatch] = useReducer((_, next) => next, 0, initializer); + const ref = useRef(null); + const onChange = e => { + dispatch(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} + +``` + +## Code + +```javascript +// @enableOptimizeForSSR + +import { useReducer } from "react"; + +const initializer = (x) => { + return x; +}; + +function Component() { + const state = initializer(0); + return ; +} + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.js new file mode 100644 index 00000000000..91844def22d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.js @@ -0,0 +1,17 @@ +// @enableOptimizeForSSR + +import {useReducer} from 'react'; + +const initializer = x => x; + +function Component() { + const [state, dispatch] = useReducer((_, next) => next, 0, initializer); + const ref = useRef(null); + const onChange = e => { + dispatch(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md new file mode 100644 index 00000000000..2bf6a02f0b5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @enableOptimizeForSSR + +import {useReducer} from 'react'; + +function Component() { + const [state, dispatch] = useReducer((_, next) => next, 0); + const ref = useRef(null); + const onChange = e => { + dispatch(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} + +``` + +## Code + +```javascript +// @enableOptimizeForSSR + +import { useReducer } from "react"; + +function Component() { + const state = 0; + return ; +} + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.js new file mode 100644 index 00000000000..4223ebe4f55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.js @@ -0,0 +1,15 @@ +// @enableOptimizeForSSR + +import {useReducer} from 'react'; + +function Component() { + const [state, dispatch] = useReducer((_, next) => next, 0); + const ref = useRef(null); + const onChange = e => { + dispatch(e.target.value); + }; + useEffect(() => { + log(ref.current.value); + }); + return ; +} diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index 02cb3775cb5..531c3cf27f6 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -487,6 +487,13 @@ const skipFilter = new Set([ 'lower-context-selector-simple', 'lower-context-acess-multiple', 'bug-separate-memoization-due-to-callback-capturing', + + // SSR optimization rewrites files in a way that causes differences or warnings + 'ssr/optimize-ssr', + 'ssr/ssr-use-reducer', + 'ssr/ssr-use-reducer-initializer', + 'ssr/infer-event-handlers-from-setState', + 'ssr/infer-event-handlers-from-startTransition', ]); export default skipFilter; From 50e7ec8a694072fd6fcd52182df8a75211bf084d Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:12:40 -0800 Subject: [PATCH 4/4] [compiler] Deprecate noEmit, add outputMode (#35112) This deprecates the `noEmit: boolean` flag and adds `outputMode: 'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement. OutputMode defaults to null and takes precedence if specified, otherwise we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true. Key points: * Retrying failed compilation switches from 'client' mode to 'client-no-memo' * Validations are enabled behind Environment.proto.shouldEnableValidations, enabled for all modes except 'client-no-memo'. Similar for dropping manual memoization. * OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag * Creation of reactive scopes, and related codegen logic, is now gated by outputMode==='client' --- .../src/Entrypoint/Options.ts | 27 ++++++- .../src/Entrypoint/Pipeline.ts | 33 ++++----- .../src/Entrypoint/Program.ts | 36 +++++++--- .../src/HIR/Environment.ts | 72 ++++++++++++++++--- .../Inference/InferMutationAliasingEffects.ts | 2 +- .../src/Optimization/DeadCodeElimination.ts | 2 +- .../ReactiveScopes/CodegenReactiveFunction.ts | 11 +-- .../gating/dynamic-gating-noemit.expect.md | 4 +- .../compiler/gating/dynamic-gating-noemit.js | 2 +- .../no-emit/retry-no-emit.expect.md | 66 ----------------- ...t-deps-with-rule-violation--lint.expect.md | 48 +++++++++++++ ...-effect-deps-with-rule-violation--lint.js} | 2 +- ...-violation-use-memo-opt-in--lint.expect.md | 49 +++++++++++++ ...h-rule-violation-use-memo-opt-in--lint.js} | 5 +- ...eps-with-rule-violation--compile.expect.md | 58 +++++++++++++++ ...fect-deps-with-rule-violation--compile.js} | 2 +- ...lation-use-memo-opt-in--compile.expect.md} | 18 ++--- ...rule-violation-use-memo-opt-in--compile.js | 21 ++++++ .../lint-repro.expect.md} | 4 +- .../lint-repro.js} | 2 +- .../new-mutability/retry-no-emit.expect.md | 66 ----------------- .../compiler/ssr/optimize-ssr.expect.md | 36 +++++++++- ...fer-event-handlers-from-setState.expect.md | 36 ++++++++-- ...nt-handlers-from-startTransition.expect.md | 40 +++++++++-- .../ssr/ssr-use-reducer-initializer.expect.md | 39 +++++++++- .../compiler/ssr/ssr-use-reducer.expect.md | 39 +++++++++- .../compiler/use-memo-noemit.expect.md | 4 +- .../fixtures/compiler/use-memo-noemit.js | 2 +- .../src/shared/RunReactCompiler.ts | 2 +- .../src/shared/RunReactCompiler.ts | 2 +- 30 files changed, 514 insertions(+), 216 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/{no-emit/retry-no-emit.js => retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.js} (86%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/{no-emit/retry-opt-in--no-emit.js => retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.js} (85%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{new-mutability/retry-no-emit.js => infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.js} (81%) rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/{no-emit/retry-opt-in--no-emit.expect.md => retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.expect.md} (77%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.js rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/{no-emit/no-emit-lint-repro.expect.md => retry-lint-comparison/lint-repro.expect.md} (86%) rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/{no-emit/no-emit-lint-repro.js => retry-lint-comparison/lint-repro.js} (83%) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 2a117b46617..b758d7b024d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -102,14 +102,25 @@ export type PluginOptions = Partial<{ panicThreshold: PanicThresholdOptions; - /* + /** + * @deprecated + * * When enabled, Forget will continue statically analyzing and linting code, but skip over codegen * passes. * + * NOTE: ignored if `outputMode` is specified + * * Defaults to false */ noEmit: boolean; + /** + * If specified, overrides `noEmit` and controls the output mode of the compiler. + * + * Defaults to null + */ + outputMode: CompilerOutputMode | null; + /* * Determines the strategy for determining which functions to compile. Note that regardless of * which mode is enabled, a component can be opted out by adding the string literal @@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([ export type CompilationMode = z.infer; +const CompilerOutputModeSchema = z.enum([ + // Build optimized for SSR, with client features removed + 'ssr', + // Build optimized for the client, with auto memoization + 'client', + // Build optimized for the client without auto memo + 'client-no-memo', + // Lint mode, the output is unused but validations should run + 'lint', +]); + +export type CompilerOutputMode = z.infer; + /** * Represents 'events' that may occur during compilation. Events are only * recorded when a logger is set (through the config). @@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = { logger: null, gating: null, noEmit: false, + outputMode: null, dynamicGating: null, eslintSuppressionRules: null, flowSuppressions: true, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index c01aceb6e8c..68b609e7f8e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -8,7 +8,7 @@ import {NodePath} from '@babel/traverse'; import * as t from '@babel/types'; import prettyFormat from 'pretty-format'; -import {Logger, ProgramContext} from '.'; +import {CompilerOutputMode, Logger, ProgramContext} from '.'; import { HIRFunction, ReactiveFunction, @@ -24,7 +24,6 @@ import { pruneUnusedLabelsHIR, } from '../HIR'; import { - CompilerMode, Environment, EnvironmentConfig, ReactFunctionType, @@ -120,7 +119,7 @@ function run( >, config: EnvironmentConfig, fnType: ReactFunctionType, - mode: CompilerMode, + mode: CompilerOutputMode, programContext: ProgramContext, logger: Logger | null, filename: string | null, @@ -170,7 +169,7 @@ function runWithEnvironment( validateUseMemo(hir).unwrap(); if ( - env.isInferredMemoEnabled && + env.enableDropManualMemoization && !env.config.enablePreserveExistingManualUseMemo && !env.config.disableMemoizationForDebugging && !env.config.enableChangeDetectionForDebugging @@ -206,7 +205,7 @@ function runWithEnvironment( inferTypes(hir); log({kind: 'hir', name: 'InferTypes', value: hir}); - if (env.isInferredMemoEnabled) { + if (env.enableValidations) { if (env.config.validateHooksUsage) { validateHooksUsage(hir).unwrap(); } @@ -232,13 +231,13 @@ function runWithEnvironment( const mutabilityAliasingErrors = inferMutationAliasingEffects(hir); log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir}); - if (env.isInferredMemoEnabled) { + if (env.enableValidations) { if (mutabilityAliasingErrors.isErr()) { throw mutabilityAliasingErrors.unwrapErr(); } } - if (env.config.enableOptimizeForSSR) { + if (env.outputMode === 'ssr') { optimizeForSSR(hir); log({kind: 'hir', name: 'OptimizeForSSR', value: hir}); } @@ -259,14 +258,14 @@ function runWithEnvironment( isFunctionExpression: false, }); log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir}); - if (env.isInferredMemoEnabled) { + if (env.enableValidations) { if (mutabilityAliasingRangeErrors.isErr()) { throw mutabilityAliasingRangeErrors.unwrapErr(); } validateLocalsNotReassignedAfterRender(hir); } - if (env.isInferredMemoEnabled) { + if (env.enableValidations) { if (env.config.assertValidMutableRanges) { assertValidMutableRanges(hir); } @@ -310,20 +309,18 @@ function runWithEnvironment( value: hir, }); - if (env.isInferredMemoEnabled) { - if (env.config.validateStaticComponents) { - env.logErrors(validateStaticComponents(hir)); - } + if (env.enableValidations && env.config.validateStaticComponents) { + env.logErrors(validateStaticComponents(hir)); + } + if (env.enableMemoization) { /** * Only create reactive scopes (which directly map to generated memo blocks) * if inferred memoization is enabled. This makes all later passes which * transform reactive-scope labeled instructions no-ops. */ - if (!env.config.enableOptimizeForSSR) { - inferReactiveScopeVariables(hir); - log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); - } + inferReactiveScopeVariables(hir); + log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); } const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir); @@ -588,7 +585,7 @@ export function compileFn( >, config: EnvironmentConfig, fnType: ReactFunctionType, - mode: CompilerMode, + mode: CompilerOutputMode, programContext: ProgramContext, logger: Logger | null, filename: string | null, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index accecc91a29..7d12e054373 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -24,6 +24,7 @@ import { validateRestrictedImports, } from './Imports'; import { + CompilerOutputMode, CompilerReactTarget, ParsedPluginOptions, PluginOptions, @@ -421,9 +422,17 @@ export function compileProgram( ); const compiledFns: Array = []; + // outputMode takes precedence if specified + const outputMode: CompilerOutputMode = + pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client'); while (queue.length !== 0) { const current = queue.shift()!; - const compiled = processFn(current.fn, current.fnType, programContext); + const compiled = processFn( + current.fn, + current.fnType, + programContext, + outputMode, + ); if (compiled != null) { for (const outlined of compiled.outlined) { @@ -581,6 +590,7 @@ function processFn( fn: BabelFn, fnType: ReactFunctionType, programContext: ProgramContext, + outputMode: CompilerOutputMode, ): null | CodegenFunction { let directives: { optIn: t.Directive | null; @@ -616,18 +626,27 @@ function processFn( } let compiledFn: CodegenFunction; - const compileResult = tryCompileFunction(fn, fnType, programContext); + const compileResult = tryCompileFunction( + fn, + fnType, + programContext, + outputMode, + ); if (compileResult.kind === 'error') { if (directives.optOut != null) { logError(compileResult.error, programContext, fn.node.loc ?? null); } else { handleError(compileResult.error, programContext, fn.node.loc ?? null); } - const retryResult = retryCompileFunction(fn, fnType, programContext); - if (retryResult == null) { + if (outputMode === 'client') { + const retryResult = retryCompileFunction(fn, fnType, programContext); + if (retryResult == null) { + return null; + } + compiledFn = retryResult; + } else { return null; } - compiledFn = retryResult; } else { compiledFn = compileResult.compiledFn; } @@ -663,7 +682,7 @@ function processFn( if (programContext.hasModuleScopeOptOut) { return null; - } else if (programContext.opts.noEmit) { + } else if (programContext.opts.outputMode === 'lint') { /** * inferEffectDependencies + noEmit is currently only used for linting. In * this mode, add source locations for where the compiler *can* infer effect @@ -693,6 +712,7 @@ function tryCompileFunction( fn: BabelFn, fnType: ReactFunctionType, programContext: ProgramContext, + outputMode: CompilerOutputMode, ): | {kind: 'compile'; compiledFn: CodegenFunction} | {kind: 'error'; error: unknown} { @@ -719,7 +739,7 @@ function tryCompileFunction( fn, programContext.opts.environment, fnType, - 'all_features', + outputMode, programContext, programContext.opts.logger, programContext.filename, @@ -757,7 +777,7 @@ function retryCompileFunction( fn, environment, fnType, - 'no_inferred_memo', + 'client-no-memo', programContext, programContext.opts.logger, programContext.filename, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 0e3654dccaf..2a0266881b0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -9,7 +9,7 @@ import * as t from '@babel/types'; import {ZodError, z} from 'zod/v4'; import {fromZodError} from 'zod-validation-error/v4'; import {CompilerError} from '../CompilerError'; -import {Logger, ProgramContext} from '../Entrypoint'; +import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint'; import {Err, Ok, Result} from '../Utils/Result'; import { DEFAULT_GLOBALS, @@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse'; import {TypeSchema} from './TypeSchema'; import {FlowTypeEnv} from '../Flood/Types'; import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider'; +import {assertExhaustive} from '../Utils/utils'; export const ReactElementSymbolSchema = z.object({ elementSymbol: z.union([ @@ -691,8 +692,6 @@ export const EnvironmentConfigSchema = z.object({ * by React to only execute in response to events, not during render. */ enableInferEventHandlers: z.boolean().default(false), - - enableOptimizeForSSR: z.boolean().default(false), }); export type EnvironmentConfig = z.infer; @@ -732,7 +731,7 @@ export class Environment { code: string | null; config: EnvironmentConfig; fnType: ReactFunctionType; - compilerMode: CompilerMode; + outputMode: CompilerOutputMode; programContext: ProgramContext; hasFireRewrite: boolean; hasInferredEffect: boolean; @@ -747,7 +746,7 @@ export class Environment { constructor( scope: BabelScope, fnType: ReactFunctionType, - compilerMode: CompilerMode, + outputMode: CompilerOutputMode, config: EnvironmentConfig, contextIdentifiers: Set, parentFunction: NodePath, // the outermost function being compiled @@ -758,7 +757,7 @@ export class Environment { ) { this.#scope = scope; this.fnType = fnType; - this.compilerMode = compilerMode; + this.outputMode = outputMode; this.config = config; this.filename = filename; this.code = code; @@ -854,8 +853,65 @@ export class Environment { return this.#flowTypeEnvironment; } - get isInferredMemoEnabled(): boolean { - return this.compilerMode !== 'no_inferred_memo'; + get enableDropManualMemoization(): boolean { + switch (this.outputMode) { + case 'lint': { + // linting drops to be more compatible with compiler analysis + return true; + } + case 'client': + case 'ssr': { + return true; + } + case 'client-no-memo': { + return false; + } + default: { + assertExhaustive( + this.outputMode, + `Unexpected output mode '${this.outputMode}'`, + ); + } + } + } + + get enableMemoization(): boolean { + switch (this.outputMode) { + case 'client': + case 'lint': { + // linting also enables memoization so that we can check if manual memoization is preserved + return true; + } + case 'ssr': + case 'client-no-memo': { + return false; + } + default: { + assertExhaustive( + this.outputMode, + `Unexpected output mode '${this.outputMode}'`, + ); + } + } + } + + get enableValidations(): boolean { + switch (this.outputMode) { + case 'client': + case 'lint': + case 'ssr': { + return true; + } + case 'client-no-memo': { + return false; + } + default: { + assertExhaustive( + this.outputMode, + `Unexpected output mode '${this.outputMode}'`, + ); + } + } } get nextIdentifierId(): IdentifierId { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index b894eb28986..4a027b87b6a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -2452,7 +2452,7 @@ function computeEffectsForLegacySignature( }), }); } - if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) { + if (signature.knownIncompatible != null && state.env.enableValidations) { const errors = new CompilerError(); errors.pushDiagnostic( CompilerDiagnostic.create({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts index 0dbc8c471be..8b251e9966c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts @@ -319,7 +319,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean { } case 'CallExpression': case 'MethodCall': { - if (state.env.config.enableOptimizeForSSR) { + if (state.env.outputMode === 'ssr') { const calleee = value.kind === 'CallExpression' ? value.callee : value.property; const hookKind = getHookKind(state.env, calleee.identifier); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index f81c962edf8..0ab7934a1a6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -159,7 +159,7 @@ export function codegenFunction( const compiled = compileResult.unwrap(); const hookGuard = fn.env.config.enableEmitHookGuards; - if (hookGuard != null && fn.env.isInferredMemoEnabled) { + if (hookGuard != null && fn.env.outputMode === 'client') { compiled.body = t.blockStatement([ createHookGuard( hookGuard, @@ -259,7 +259,7 @@ export function codegenFunction( if ( emitInstrumentForget != null && fn.id != null && - fn.env.isInferredMemoEnabled + fn.env.outputMode === 'client' ) { /* * Technically, this is a conditional hook call. However, we expect @@ -591,7 +591,10 @@ function codegenBlockNoReset( } function wrapCacheDep(cx: Context, value: t.Expression): t.Expression { - if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) { + if ( + cx.env.config.enableEmitFreeze != null && + cx.env.outputMode === 'client' + ) { const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier( cx.env.config.enableEmitFreeze, ).name; @@ -1772,7 +1775,7 @@ function createCallExpression( } const hookGuard = env.config.enableEmitHookGuards; - if (hookGuard != null && isHook && env.isInferredMemoEnabled) { + if (hookGuard != null && isHook && env.outputMode === 'client') { const iife = t.functionExpression( null, [], diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md index 81ebd6dd9fa..c00e78b6e7b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @dynamicGating:{"source":"shared-runtime"} @noEmit +// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint" function Foo() { 'use memo if(getTrue)'; @@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -// @dynamicGating:{"source":"shared-runtime"} @noEmit +// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint" function Foo() { "use memo if(getTrue)"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js index 97cf777a552..901a1dd3eae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js @@ -1,4 +1,4 @@ -// @dynamicGating:{"source":"shared-runtime"} @noEmit +// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint" function Foo() { 'use memo if(getTrue)'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md deleted file mode 100644 index 55a6aa5c46d..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md +++ /dev/null @@ -1,66 +0,0 @@ - -## Input - -```javascript -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly -import {print} from 'shared-runtime'; -import useEffectWrapper from 'useEffectWrapper'; -import {AUTODEPS} from 'react'; - -function Foo({propVal}) { - const arr = [propVal]; - useEffectWrapper(() => print(arr), AUTODEPS); - - const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), AUTODEPS); - arr2.push(2); - return {arr, arr2}; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{propVal: 1}], - sequentialRenders: [{propVal: 1}, {propVal: 2}], -}; - -``` - -## Code - -```javascript -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly -import { print } from "shared-runtime"; -import useEffectWrapper from "useEffectWrapper"; -import { AUTODEPS } from "react"; - -function Foo({ propVal }) { - const arr = [propVal]; - useEffectWrapper(() => print(arr), AUTODEPS); - - const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), AUTODEPS); - arr2.push(2); - return { arr, arr2 }; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ propVal: 1 }], - sequentialRenders: [{ propVal: 1 }, { propVal: 2 }], -}; - -``` - -## Logs - -``` -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":372},"end":{"line":12,"column":6,"index":376},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}} -{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":248},"end":{"line":8,"column":46,"index":292},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":277},"end":{"line":8,"column":34,"index":280},"filename":"retry-no-emit.ts","identifierName":"arr"}]} -{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":316},"end":{"line":11,"column":54,"index":368},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":349},"end":{"line":11,"column":42,"index":356},"filename":"retry-no-emit.ts","identifierName":"propVal"}]} -{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} -``` - -### Eval output -(kind: ok) {"arr":[1],"arr2":[2]} -{"arr":[2],"arr2":[2]} -logs: [[ 1 ],[ 2 ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.expect.md new file mode 100644 index 00000000000..b8d213b6374 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.expect.md @@ -0,0 +1,48 @@ + +## Input + +```javascript +// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none" +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot infer dependencies of this effect. This will break your build! + +To resolve, either pass a dependency array or fix reported compiler bailout diagnostics. + +error.infer-effect-deps-with-rule-violation--lint.ts:8:2 + 6 | function Foo({propVal}) { + 7 | const arr = [propVal]; +> 8 | useEffectWrapper(() => print(arr), AUTODEPS); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies + 9 | + 10 | const arr2 = []; + 11 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.js similarity index 86% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.js index 692653b4174..7df86cdfd25 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation--lint.js @@ -1,4 +1,4 @@ -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly +// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none" import {print} from 'shared-runtime'; import useEffectWrapper from 'useEffectWrapper'; import {AUTODEPS} from 'react'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.expect.md new file mode 100644 index 00000000000..80d8af00e61 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none" +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + 'use memo'; + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot infer dependencies of this effect. This will break your build! + +To resolve, either pass a dependency array or fix reported compiler bailout diagnostics. + +error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.ts:9:2 + 7 | 'use memo'; + 8 | const arr = [propVal]; +> 9 | useEffectWrapper(() => print(arr), AUTODEPS); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies + 10 | + 11 | const arr2 = []; + 12 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.js similarity index 85% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.js index e4ec1a545fe..42bbf4c994b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.js @@ -1,7 +1,7 @@ -// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit +// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none" import {print} from 'shared-runtime'; -import {AUTODEPS} from 'react'; import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; function Foo({propVal}) { 'use memo'; @@ -11,7 +11,6 @@ function Foo({propVal}) { const arr2 = []; useEffectWrapper(() => arr2.push(propVal), AUTODEPS); arr2.push(2); - return {arr, arr2}; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.expect.md new file mode 100644 index 00000000000..47de4a1d193 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" +import { print } from "shared-runtime"; +import useEffectWrapper from "useEffectWrapper"; +import { AUTODEPS } from "react"; + +function Foo(t0) { + const { propVal } = t0; + const arr = [propVal]; + useEffectWrapper(() => print(arr), [arr]); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]); + arr2.push(2); + return { arr, arr2 }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ propVal: 1 }], + sequentialRenders: [{ propVal: 1 }, { propVal: 2 }], +}; + +``` + +### Eval output +(kind: ok) {"arr":[1],"arr2":[2]} +{"arr":[2],"arr2":[2]} +logs: [[ 1 ],[ 2 ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.js similarity index 81% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.js index 2815a9a28d1..6cca9a833d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation--compile.js @@ -1,4 +1,4 @@ -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +// @inferEffectDependencies @panicThreshold:"none" import {print} from 'shared-runtime'; import useEffectWrapper from 'useEffectWrapper'; import {AUTODEPS} from 'react'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.expect.md similarity index 77% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.expect.md index a2d5610accf..610ad348902 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.expect.md @@ -2,10 +2,10 @@ ## Input ```javascript -// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit +// @inferEffectDependencies @panicThreshold:"none" import {print} from 'shared-runtime'; -import {AUTODEPS} from 'react'; import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; function Foo({propVal}) { 'use memo'; @@ -15,7 +15,6 @@ function Foo({propVal}) { const arr2 = []; useEffectWrapper(() => arr2.push(propVal), AUTODEPS); arr2.push(2); - return {arr, arr2}; } @@ -30,20 +29,21 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit +// @inferEffectDependencies @panicThreshold:"none" import { print } from "shared-runtime"; -import { AUTODEPS } from "react"; import useEffectWrapper from "useEffectWrapper"; +import { AUTODEPS } from "react"; -function Foo({ propVal }) { +function Foo(t0) { "use memo"; + const { propVal } = t0; + const arr = [propVal]; - useEffectWrapper(() => print(arr), AUTODEPS); + useEffectWrapper(() => print(arr), [arr]); const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]); arr2.push(2); - return { arr, arr2 }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.js new file mode 100644 index 00000000000..efa5db19406 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/infer-effect-deps-with-rule-violation-use-memo-opt-in--compile.js @@ -0,0 +1,21 @@ +// @inferEffectDependencies @panicThreshold:"none" +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + 'use memo'; + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.expect.md similarity index 86% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.expect.md index 7c8142f436d..1d767ce3db7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @inferEffectDependencies @noEmit +// @inferEffectDependencies @outputMode:"lint" import {print} from 'shared-runtime'; import useEffectWrapper from 'useEffectWrapper'; import {AUTODEPS} from 'react'; @@ -17,7 +17,7 @@ function ReactiveVariable({propVal}) { ## Code ```javascript -// @inferEffectDependencies @noEmit +// @inferEffectDependencies @outputMode:"lint" import { print } from "shared-runtime"; import useEffectWrapper from "useEffectWrapper"; import { AUTODEPS } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.js similarity index 83% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.js index 0df8291afdc..011cc535c21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/no-emit-lint-repro.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/retry-lint-comparison/lint-repro.js @@ -1,4 +1,4 @@ -// @inferEffectDependencies @noEmit +// @inferEffectDependencies @outputMode:"lint" import {print} from 'shared-runtime'; import useEffectWrapper from 'useEffectWrapper'; import {AUTODEPS} from 'react'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md deleted file mode 100644 index 9529a5c31de..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md +++ /dev/null @@ -1,66 +0,0 @@ - -## Input - -```javascript -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel -import {print} from 'shared-runtime'; -import useEffectWrapper from 'useEffectWrapper'; -import {AUTODEPS} from 'react'; - -function Foo({propVal}) { - const arr = [propVal]; - useEffectWrapper(() => print(arr), AUTODEPS); - - const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), AUTODEPS); - arr2.push(2); - return {arr, arr2}; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{propVal: 1}], - sequentialRenders: [{propVal: 1}, {propVal: 2}], -}; - -``` - -## Code - -```javascript -// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel -import { print } from "shared-runtime"; -import useEffectWrapper from "useEffectWrapper"; -import { AUTODEPS } from "react"; - -function Foo({ propVal }) { - const arr = [propVal]; - useEffectWrapper(() => print(arr), AUTODEPS); - - const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), AUTODEPS); - arr2.push(2); - return { arr, arr2 }; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ propVal: 1 }], - sequentialRenders: [{ propVal: 1 }, { propVal: 2 }], -}; - -``` - -## Logs - -``` -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":404},"end":{"line":12,"column":6,"index":408},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}} -{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":280},"end":{"line":8,"column":46,"index":324},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":309},"end":{"line":8,"column":34,"index":312},"filename":"retry-no-emit.ts","identifierName":"arr"}]} -{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":348},"end":{"line":11,"column":54,"index":400},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":381},"end":{"line":11,"column":42,"index":388},"filename":"retry-no-emit.ts","identifierName":"propVal"}]} -{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} -``` - -### Eval output -(kind: ok) {"arr":[1],"arr2":[2]} -{"arr":[2],"arr2":[2]} -logs: [[ 1 ],[ 2 ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md index 3508aab5358..48a0a92be70 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md @@ -20,10 +20,40 @@ function Component() { ## Code ```javascript -// @enableOptimizeForSSR +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR function Component() { - const state = 0; - return ; + const $ = _c(4); + const [state, setState] = useState(0); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = (e) => { + setState(e.target.value); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onChange = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + log(ref.current.value); + }; + $[1] = t1; + } else { + t1 = $[1]; + } + useEffect(t1); + let t2; + if ($[2] !== state) { + t2 = ; + $[2] = state; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md index 0aeb890c26d..80884d84530 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md @@ -22,12 +22,40 @@ function Component() { ## Code ```javascript -// @enableOptimizeForSSR +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR function Component() { - const state = 0; + const $ = _c(4); + const [state, setState] = useState(0); const ref = useRef(null); - const onChange = undefined; - return ; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = (e) => { + setState(e.target.value); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onChange = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + log(ref.current.value); + }; + $[1] = t1; + } else { + t1 = $[1]; + } + useEffect(t1); + let t2; + if ($[2] !== state) { + t2 = ; + $[2] = state; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md index 53cf10a678a..ccfdccb2888 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md @@ -25,13 +25,43 @@ function Component() { ## Code ```javascript -// @enableOptimizeForSSR +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR function Component() { - useTransition(); - const state = 0; + const $ = _c(4); + const [, startTransition] = useTransition(); + const [state, setState] = useState(0); const ref = useRef(null); - const onChange = undefined; - return ; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = (e) => { + startTransition(() => { + setState.call(null, e.target.value); + }); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onChange = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + log(ref.current.value); + }; + $[1] = t1; + } else { + t1 = $[1]; + } + useEffect(t1); + let t2; + if ($[2] !== state) { + t2 = ; + $[2] = state; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md index ead89e12887..780e1f3963c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md @@ -25,7 +25,7 @@ function Component() { ## Code ```javascript -// @enableOptimizeForSSR +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR import { useReducer } from "react"; @@ -34,8 +34,41 @@ const initializer = (x) => { }; function Component() { - const state = initializer(0); - return ; + const $ = _c(4); + const [state, dispatch] = useReducer(_temp, 0, initializer); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = (e) => { + dispatch(e.target.value); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onChange = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + log(ref.current.value); + }; + $[1] = t1; + } else { + t1 = $[1]; + } + useEffect(t1); + let t2; + if ($[2] !== state) { + t2 = ; + $[2] = state; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp(_, next) { + return next; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md index 2bf6a02f0b5..3c48b27f861 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md @@ -23,13 +23,46 @@ function Component() { ## Code ```javascript -// @enableOptimizeForSSR +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR import { useReducer } from "react"; function Component() { - const state = 0; - return ; + const $ = _c(4); + const [state, dispatch] = useReducer(_temp, 0); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = (e) => { + dispatch(e.target.value); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onChange = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + log(ref.current.value); + }; + $[1] = t1; + } else { + t1 = $[1]; + } + useEffect(t1); + let t2; + if ($[2] !== state) { + t2 = ; + $[2] = state; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp(_, next) { + return next; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md index c47501945b3..2068971957c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @noEmit +// @outputMode:"lint" function Foo() { 'use memo'; @@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -// @noEmit +// @outputMode:"lint" function Foo() { "use memo"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.js index 04ec8807616..b12668f15ae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.js @@ -1,4 +1,4 @@ -// @noEmit +// @outputMode:"lint" function Foo() { 'use memo'; diff --git a/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts b/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts index 419dc3841c7..aa55c64237e 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts @@ -21,7 +21,7 @@ import {isDeepStrictEqual} from 'util'; import type {ParseResult} from '@babel/parser'; const COMPILER_OPTIONS: PluginOptions = { - noEmit: true, + outputMode: 'lint', panicThreshold: 'none', // Don't emit errors on Flow suppressions--Flow already gave a signal flowSuppressions: false, diff --git a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts index 02fd68badf6..f2062496feb 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts @@ -22,7 +22,7 @@ import {isDeepStrictEqual} from 'util'; import type {ParseResult} from '@babel/parser'; const COMPILER_OPTIONS: PluginOptions = { - noEmit: true, + outputMode: 'lint', panicThreshold: 'none', // Don't emit errors on Flow suppressions--Flow already gave a signal flowSuppressions: false,