From 5fa52005611ddebeca8eef42a73b341c7e16ffb4 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 13 Nov 2025 15:09:58 -0800 Subject: [PATCH] [compiler][poc] Quick experiment with SSR-optimization pass 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. --- .../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 132507f41a3d2..c01aceb6e8c98 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 bb19eab93cfea..24843c115fece 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -677,6 +677,8 @@ export const EnvironmentConfigSchema = z.object({ * from refs need to be stored in state during mount. */ enableAllowSetStateFromRefsInEffects: z.boolean().default(true), + + 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 41e957a54677f..bda068a6d1634 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 2b752c6dfd28e..0dbc8c471beb2 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 0000000000000..3fd7db1fc6db0 --- /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 0000000000000..3508aab535843 --- /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 0000000000000..d9fba0f390339 --- /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 0000000000000..0aeb890c26de2 --- /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 0000000000000..c67f026c040c8 --- /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 0000000000000..53cf10a678af5 --- /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 0000000000000..f6f6f3914dc72 --- /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 0000000000000..ead89e12887ff --- /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 0000000000000..91844def22da3 --- /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 0000000000000..2bf6a02f0b5ad --- /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 0000000000000..4223ebe4f5592 --- /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 02cb3775cb549..531c3cf27f63f 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;