Inference: Types & Mutation Effects
Overview
The React Compiler utilizes a sophisticated inference engine to determine the lifecycle and reactivity of variables within a component or hook. By analyzing how values are created, mutated, and where they "escape" the local function scope, the compiler can automatically insert memoization boundaries (reactive scopes) only where necessary.
This process relies on two primary pillars: Type Inference (distinguishing between primitives and allocable objects) and Mutation Aliasing Analysis (tracking how values flow into one another).
Type Inference and Primitives
The compiler distinguishes between primitive values and complex objects to optimize the generated code.
- Primitives: Values such as strings, numbers, and booleans are considered "natively" stable if their source inputs haven't changed. The compiler often prunes reactive scopes for primitives because the cost of memoizing a primitive (storing it and comparing it) often exceeds the cost of recomputing it.
- Allocations: Arrays, objects, and function expressions are treated as allocations. These require reactive scopes to maintain referential identity across renders, ensuring that downstream components or hooks (like
useMemooruseEffect) do not trigger unnecessarily.
function Component(props) {
// Primitive: The compiler may prune this scope as it is non-escaping
// and cheap to re-calculate.
const a = props.a + props.b;
// Allocation: This requires a reactive scope to maintain identity.
const b = { value: a };
return b;
}
Escape Analysis
A value is said to "escape" when it is used in a way that makes its lifetime observable outside the immediate scope of its creation. The compiler marks identifiers as escaping based on three main criteria:
- Return Values: Any identifier directly returned by the function or transitively aliased by a return value.
- Hook Arguments: Values passed as inputs to hooks (e.g.,
useEffect,useCallback). Since React may store these values internally, the compiler treats them as having escaped to an external reference. - Transitive Aliasing: If a value
Ais assigned to a property of valueB, andBescapes, thenAis also marked as escaping.
Pruning Non-Escaping Scopes
To reduce code size and overhead, the compiler performs a pruning pass (PruneNonEscapingScopes). If a reactive scope produces values that never escape and are not dependencies of other escaping values, the scope is removed.
However, the compiler preserves scopes for non-escaping values if they are dependencies of an escaping scope. Failing to memoize a non-escaping dependency would cause the downstream escaping scope to invalidate on every render.
function Component(props) {
// 'a' does not escape, but it is a dependency of 'b'.
// If we don't memoize 'a', 'b' will recreate every render.
const a = [props.a];
const b = [];
const c = {};
c.internal = a; // 'a' is now aliased by 'c'
b.push(props.b);
return b; // 'b' escapes, therefore 'a' must be memoized.
}
Mutation and Aliasing Effects
The compiler's High-Level Intermediate Representation (HIR) tracks mutation effects to group instructions into "Reactive Scopes."
- Interleaved Mutations: When multiple independent values are mutated within the same logical block or interleave their mutation instructions, the compiler groups them into a single reactive scope.
- Aliasing Graph: The compiler builds a graph of
IdentifierIdnodes. Nodes are marked as:- Definitely Aliased: Created by arrays, objects, or function calls.
- Conditionally Aliased: Created by logical or conditional expressions (e.g.,
a || b). - Unaliased: Specifically for JSX elements (while their props may be aliased, the element itself is a fresh allocation).
Validation Constraints
To ensure the safety of its inferences, the compiler enforces specific architectural rules during the analysis phase.
Capitalized Function Calls
The compiler reserves capitalized function calls for React Components, which must be invoked via JSX. Calling a capitalized function directly (e.g., const x = MyComponent()) is flagged as an error to prevent the compiler from incorrectly inferring hook usage or component lifecycles.
// @validateNoCapitalizedCalls
function Component() {
// ERROR: Capitalized functions are reserved for components.
// This could be a component with hooks, which must be rendered via JSX.
const x = SomeFunc();
return <div />;
}
Hook Pattern Matching
The inference engine uses configurable patterns (defaulting to use...) to identify hook calls. Values returned from hooks are treated as potentially reactive and are tracked through the aliasing graph to ensure that any derived state is correctly encapsulated in reactive boundaries.
Summary of Inference Logic
| Phase | Technical Logic |
| :--- | :--- |
| Collection | Build a map of instructions creating values and identify return/hook sites. |
| Graph Construction | Map IdentifierId to nodes describing scopes and dependencies. |
| Propagation | Traverse the graph from returned identifiers to mark all reachable dependencies as "escaping." |
| Pruning | Remove scopes whose outputs were not marked as escaping, unless they are required to stabilize a downstream dependency. |
| Validation | Verify no capitalized calls or invalid hook patterns break the execution model. |