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 132507f41a3..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, @@ -105,6 +104,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 = @@ -119,7 +119,7 @@ function run( >, config: EnvironmentConfig, fnType: ReactFunctionType, - mode: CompilerMode, + mode: CompilerOutputMode, programContext: ProgramContext, logger: Logger | null, filename: string | null, @@ -169,7 +169,7 @@ function runWithEnvironment( validateUseMemo(hir).unwrap(); if ( - env.isInferredMemoEnabled && + env.enableDropManualMemoization && !env.config.enablePreserveExistingManualUseMemo && !env.config.disableMemoizationForDebugging && !env.config.enableChangeDetectionForDebugging @@ -205,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(); } @@ -231,12 +231,17 @@ 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.outputMode === 'ssr') { + 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}); @@ -253,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); } @@ -304,11 +309,11 @@ 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 @@ -580,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 b29d65e6aa5..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([ @@ -730,7 +731,7 @@ export class Environment { code: string | null; config: EnvironmentConfig; fnType: ReactFunctionType; - compilerMode: CompilerMode; + outputMode: CompilerOutputMode; programContext: ProgramContext; hasFireRewrite: boolean; hasInferredEffect: boolean; @@ -745,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 @@ -756,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; @@ -852,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/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/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 2b752c6dfd2..8b251e9966c 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.outputMode === 'ssr') { + 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/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/Validation/ValidateNoDerivedComputationsInEffects_exp.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts index af5927548af..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 @@ -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) { @@ -372,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, @@ -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} +
+ ); +} 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} ``` 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 new file mode 100644 index 00000000000..48a0a92be70 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md @@ -0,0 +1,60 @@ + +## 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 +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR +function Component() { + 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; +} + +``` + \ 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..80884d84530 --- /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,64 @@ + +## 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 +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR +function Component() { + 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; +} + +``` + +### 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..ccfdccb2888 --- /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,70 @@ + +## 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 +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR +function Component() { + const $ = _c(4); + const [, startTransition] = useTransition(); + const [state, setState] = useState(0); + const ref = useRef(null); + 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; +} + +``` + +### 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..780e1f3963c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md @@ -0,0 +1,75 @@ + +## 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 +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR + +import { useReducer } from "react"; + +const initializer = (x) => { + return x; +}; + +function Component() { + 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; +} + +``` + \ 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..3c48b27f861 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md @@ -0,0 +1,69 @@ + +## 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 +import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR + +import { useReducer } from "react"; + +function Component() { + 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; +} + +``` + \ 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/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/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; 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,