Scope Alignment & Merging
The React Compiler (often referred to as React Forget) automatically optimizes your components by identifying "reactive scopes." These scopes define the boundaries for memoization, ensuring that expensive computations and object allocations only re-run when their dependencies change.
Understanding how the compiler aligns these scopes to your code and merges interleaved logic is essential for writing performant, compiler-compatible React.
How Reactive Scopes are Defined
A reactive scope is a grouping of instructions that produce one or more values. The compiler analyzes your code to determine the "lifetime" of a value—from its initial declaration to its last mutation.
Scope Alignment
The compiler aligns scopes based on data flow and mutation. Every value produced in a component is evaluated to see if it needs to be "captured" in a scope.
- Declarative alignment: If a value is used in a way that suggests it should be stable (e.g., passed as a prop to a memoized component or used as a dependency in a hook), the compiler creates a scope for it.
- Block-level boundaries: Scopes generally respect JavaScript block boundaries but can span multiple lines to capture all mutations of a specific object.
Merging Interleaved Mutations
One of the most complex aspects of automatic memoization is handling interleaved mutations. If multiple values are mutated in an overlapping sequence, the compiler merges them into a single reactive scope.
Why Merging Occurs
If two independent objects, objA and objB, are mutated in a way that their logic is physically mixed, they must share a memoization boundary. This ensures that the state of both objects remains consistent relative to each other.
function Component(props) {
// These two values will be merged into a single scope
// because their mutations interleave.
const objA = {};
const objB = [];
objA.name = props.name;
objB.push(props.id);
objA.updated = true;
return <Child a={objA} b={objB} />;
}
In this example, the compiler cannot safely memoize objA without also considering the state of objB because the instructions are interleaved. They are grouped together to ensure that any change in props.name or props.id correctly invalidates the entire scope.
Pruning Non-Escaping Scopes
To keep the compiled output efficient and minimize code size, the compiler performs "Escape Analysis." It prunes (removes) reactive scopes for values that do not "escape" the local execution context.
What Counts as Escaping?
A value is considered to "escape" if:
- It is returned by the component or hook.
- It is passed to a hook (e.g.,
useEffect(callback, [val])), as the hook might store a reference to the value. - It is transitive: If
valueAis assigned to a property ofvalueB, andvalueBescapes, thenvalueAalso escapes.
Dependency Preservation
Even if a value does not escape, the compiler may decide not to prune its scope if it is a dependency of another scope that does escape.
function Component(props) {
// 'a' does not escape directly, but it is a dependency of 'b'.
// If we don't memoize 'a', 'b' would be re-created every render.
const a = [props.a];
const b = { data: props.b };
b.ref = a; // 'a' is now a dependency of an escaping value
return b; // 'b' escapes
}
In this case, the compiler preserves the scope for a to ensure that b remains stable across renders.
Validation and Restrictions
To ensure scopes can be safely aligned and merged, the compiler enforces specific coding patterns.
Capitalized Function Calls
The compiler distinguishes between regular JavaScript functions and React Components/Hooks using naming conventions. By default, calling a capitalized function directly is treated as a potential error because React components should be invoked via JSX.
// 🔴 Error: Capitalized functions are reserved for components
function Component() {
const x = SomeFunction();
return <div>{x}</div>;
}
// ✅ Correct: Render as JSX
function Component() {
return <SomeFunction />;
}
If you must call a capitalized function that is not a component (e.g., a legacy library utility), you can allowlist specific names in your compiler configuration:
{
"validateNoCapitalizedCalls": ["EXTERNAL_LIB_CONSTANT"]
}
Hook Patterns
The compiler uses a hookPattern (defaulting to the use[A-Z] convention) to identify which calls are hooks. Scopes are aligned differently around hooks because hooks must always execute in the same order and cannot be wrapped in conditional logic or loops created by the compiler.