Reactive Scopes & Codegen
Reactive scopes are the compiler’s units for grouping instructions that can be memoized together. The compiler first turns HIR control flow into a tree-shaped ReactiveFunction, then tracks which values mutate together and assigns them scopes.
The final code-generation pass converts that reactive tree back into a Babel-compatible function, allocating cache slots and emitting useMemoCache when memoized values exist.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:42-57, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:86-172, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:333-373, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:163-179
Core concepts
Reactive scope
A reactive scope is a range of instructions whose declarations and dependencies are evaluated as one memoizable unit. Scope inference groups identifiers that mutate together, records a mutable instruction range, and stores dependencies, declarations, and reassignments on the scope.
Reactive block
A reactive block is the ordered instruction structure that codegen consumes. Its items may be ordinary instructions, nested scopes, pruned scopes, or control-flow terminals.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:501-558
Dependency
A dependency is an identifier, optionally followed by a property path, whose cached value is compared to decide whether a scope must run again.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:1246-1272, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:575-603
Memo cache slot
A memo cache slot is an indexed position in the array returned by useMemoCache; codegen uses slots for dependency snapshots and scope outputs.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:65-107, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:575-639
How scopes are built from instructions
buildReactiveFunction creates a Context, constructs a Driver, traverses the HIR entry block, and returns a ReactiveFunction containing the resulting body and the original function metadata.
The traversal preserves value-producing instruction sequences rather than treating every control-flow block as an isolated expression. visitValueBlock follows branches, gotos, and maybe-throw continuations, extracting the final instruction as the value or wrapping preceding instructions into a sequence.
When a value block has preceding instructions, wrapWithSequence keeps those instructions together with the continuation’s value and place. extractValueBlockResult similarly uses the last instruction as the result and places earlier instructions into a SequenceExpression; nested sequences can later be flattened by valueBlockResultToSequence.
Conditionals are represented by visiting a test value block and then retaining its consequent and alternate block IDs. This gives later passes an explicit branch structure instead of losing the control-flow boundary during value extraction.
The resulting reactive block can therefore contain both executable instructions and structural terminals. Codegen handles ordinary instructions directly, recursively emits nested scopes, and converts terminals into control-flow statements or labeled statements when required.
Evidence
- hir-functioncompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:42
- buildercompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:42
- drivercompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:199
- value-blockcompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:894
- reactive-blockcompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:501
- control-flowcompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:994
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:42-57, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:894-987, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:71-97, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:107-142, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:152-197, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts:994-1024, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:501-558
How the compiler decides scope boundaries
findDisjointMutableValues builds disjoint identifier groups representing values that mutate together. It accounts for loop-carried and phi-related mutation, and it includes instructions that may allocate or mutate relevant values.
inferReactiveScopeVariables assigns each group a scope ID and combines the members’ mutable ranges into one range. It also initializes each scope with dependency, declaration, reassignment, early-return, and merge state.
The compiler is conservative about allocation: literals and many loads do not count as allocating, while object and array expressions, function expressions, and several store or property operations do. Calls are allocation-sensitive when their result is not primitive.
A scope is eligible for merging when it has no dependencies, because its value cannot change, or when one of its declarations has a type that always invalidates with its inputs. Built-in arrays, objects, functions, JSX values, and function types are treated as always-invalidating types.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:274-417, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:86-172, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:209-272, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:560-571, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:505-523
Why scopes that invalidate together are merged
mergeReactiveScopesThatInvalidateTogether performs a last-usage analysis and then transforms the reactive function using those usage positions. The last-usage visitor records the latest instruction at which each declaration is used.
The merge transform first visits nested blocks, then scans each block for consecutive scopes. It does not merge across terminals or pruned scopes, and it only considers intervening instructions from a conservative set of simple value kinds.
Two scopes can merge when their dependencies are identical. They can also merge when the previous scope’s outputs become the next scope’s inputs, but only when those outputs are guaranteed to invalidate with their inputs. This avoids separating memoization units that would always be invalidated together, reducing memoization overhead without making a potentially stable output conditional.
The transform also rejects unsafe cases: scopes with reassignments cannot merge, and an intervening value cannot be moved into the earlier scope if it is used after that scope ends.
After a merge, declarations whose last use occurs before the merged scope’s end are removed from the scope declaration set. This keeps the merged scope’s output surface limited to values still needed afterward.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:86-92, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:101-119, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:145-398, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:437-503, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:422-435, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:405-415
How CodegenReactiveFunction emits memoized output
codegenFunction creates a code-generation Context, invokes codegenReactiveFunction, and then optionally wraps the generated body with hook guards for client output.
codegenReactiveFunction declares parameters, converts the reactive body with codegenBlock, preserves directives, removes a trailing empty return, and returns a CodegenFunction. Its result records the number of cache slots, memo blocks, memoized values, and pruned memoization blocks.
codegenBlockNoReset walks the reactive block. Instructions become statements, pruned scopes are emitted inline, scopes go through codegenReactiveScope, and terminals go through codegenTerminal. A surrounding codegenBlock snapshots temporary bindings and verifies that existing temporaries were not changed while the block was emitted.
For each scope dependency, codegenReactiveScope allocates a cache index, compares the cached entry with the generated dependency expression, and stores the current dependency back into that slot. It then allocates further slots for scope declarations and reassignments, establishing the cache locations for outputs.
If the function uses memo slots, codegenFunction requests the useMemoCache import and emits a call with the total cache count before the generated body. The cache sentinel used by this codegen module is react.memo_cache_sentinel.
Instruction values are converted into Babel expressions through codegenInstructionValueToExpression, which delegates to codegenInstructionValue and normalizes JSX text into string literals when necessary. Places resolve through the codegen context’s temporary map or through named identifiers.
Codegen also reconstructs declarations and assignments. A first write becomes a variable declaration, while a previously declared identifier becomes an assignment; unnamed temporaries are kept in the context rather than emitted as source variables.
Evidence
- codegen-entrycompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:109
- reactive-functioncompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:333
- block-emittercompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:474
- scope-emittercompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:560
- cachecompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:163
- babel-outputcompiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:65
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:109-331, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:333-373, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:65-107, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:501-558, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:474-492, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:560-639, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:163-179, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:62, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:1477-1483, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:2414-2429, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:1431-1466
How it connects
The reactive-scope package converts HIR control flow into a tree-structured ReactiveFunction and runs scope-related transformations such as pruning and merging. The Rust implementation identifies CodegenReactiveFunction as the final pass that converts the reactive tree into a Babel-compatible AST with useMemoCache wiring.
The Rust compiler pipeline invokes merge_reactive_scopes_that_invalidate_together before continuing to later optimization work. For broader compiler ordering and the surrounding passes, see Compiler Overview & Pipeline and Compiler HIR & Passes. For the Babel transform boundary, see Babel Plugin Integration.
Sources: compiler/crates/react_compiler_reactive_scopes/src/lib.rs:6-18, compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs:6-11, compiler/crates/react_compiler/src/entrypoint/pipeline.rs:890-910
Key takeaways
- A reactive scope groups instructions, declarations, dependencies, and mutable ranges into one memoizable unit.
- Scopes merge when they invalidate together, but reassignments, later uses, terminals, and pruned scopes constrain merging.
codegenReactiveFunctionrebuilds the function body and reports cache and memoization counts.codegenReactiveScopemaps dependencies and outputs to indexed cache slots.codegenFunctionemitsuseMemoCachewhen the generated function needs memo slots.
Sources: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts:86-172, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:145-398, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts:437-503, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:333-373, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:560-639, compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts:163-179