The Mutability & Aliasing Model

This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).

To build an intuition, consider the following example:

function Component() {
    // a is created and mutated over the course of these two instructions:
    const a = {};
    mutate(a);

    // b and c are created and mutated together — mutate might modify b via c
    const b = {};
    const c = {b};
    mutate(c);

    // does not modify a/b/c
    return <Foo a={a} c={c} />
}

The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.

In code, the mutability and aliasing model is compromised of the following phases:

Finally, AnalyzeFunctions needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. AnalyzeFunctions calls InferFunctionExpressionAliasingEffectsSignature to determine the publicly observable set of mutation/aliasing effects for nested functions.

Mutation and Aliasing Effects

The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time

Creation Effects

Create

{
    kind: 'Create';
    into: Place;
    value: ValueKind;
    reason: ValueReason;
}

Describes the creation of a new value with the given kind, and reason for having that kind. For example, x = 10 might have an effect like Create x = ValueKind.Primitive [ValueReason.Other].

CreateFunction

{
    kind: 'CreateFunction';
    captures: Array<Place>;
    function: FunctionExpression | ObjectMethod;
    into: Place;
}

Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with Apply.

Apply

{
    kind: 'Apply';
    receiver: Place;
    function: Place; // same as receiver for function calls
    mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
    args: Array<Place | SpreadPattern | Hole>;
    into: Place; // where result is stored
    signature: FunctionSignature | null;
}

Describes the potential creation of a value by calling a function. This models new, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:

The generic fallback is to assume:

Aliasing Effects

These effects describe data-flow only, separately from mutation or other state-changing semantics.

Assign

{
    kind: 'Assign';
    from: Place;
    into: Place;
}

Describes an x = y assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that Alias initializes the receiving value.

TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect

Alias

{
    kind: 'Alias';
    from: Place;
    into: Place;
}

Describes that an assignment may occur, but that the possible assignment is non-exclusive. The canonical use-case for Alias is a function that may return more than one of its arguments, such as (x, y, z) => x ? y : z. Here, the result of this function may be y or z, but neither one overwrites the other. Note that Alias does not initialize the receiving value: it should always be paired with an effect to create the receiving value.

Capture

{
    kind: 'Capture';
    from: Place;
    into: Place;
}

Describes that a reference to one variable (from) is stored within another value (into). Examples include:

CreateFrom

{
    kind: 'CreateFrom';
    from: Place;
    into: Place;
}

This is somewhat the inverse of Capture. The CreateFrom effect describes that a variable is initialized by extracting part of another value, without taking a direct alias to the full other value. Examples include:

ImmutableCapture

Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.

State-Changing Effects

The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects and state-change effects. For example, object.property = value has data flow (Capture object <- value) and mutation (Mutate object).

Freeze

{
    kind: 'Freeze',
    // The reference being frozen
    value: Place;
    // The reason the value is frozen (passed to a hook, passed to jsx, etc)
    reason: ValueReason;
}

Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:

// INVALID DO NOT DO THIS
function Component(props) {
    const array = useArray(props.value);
    // OOPS! this value is memoized, the array won't get re-created
    // when `props.value` changes, so we might just keep pushing new
    // values to the same array on every render!
    array.push(props.otherValue);
}

function useArray(a) {
    return useMemo(() => [a], [a]);
}

The Freeze effect accepts a variable reference and a reason that the value is being frozen. Note: freeze only applies to the reference, not the underlying value. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:

const x = {};
const y = [];
x.y = y;
freeze(y); // y _reference_ is frozen
x.y.push(props.value); // but y is still considered mutable bc of this

Mutate (and MutateConditionally)

{
    kind: 'Mutate';
    value: Place;
}

Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:

This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.

MutateConditionally is an alternative in which the mutation may happen depending on the type of the value. The conditional variant is not generally used and included for completeness.

MutateTransitiveConditionally (and MutateTransitive)

MutateTransitiveConditionally represents an operation that may mutate any aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.

There is also MutateTransitive for completeness, but this is not generally used.

Side Effects

Finally, there are a few effects that describe error, or potential error, conditions:

Rules

Mutation of Alias Mutates the Source Value

Alias a <- b
Mutate a
=>
Mutate b

Example:

const a = maybeIdentity(b); // Alias a <- b
a.property = value; // a could be b, so this mutates b

Mutation of Assignment Mutates the Source Value

Assign a <- b
Mutate a
=>
Mutate b

Example:

const a = b;
a.property = value // a _is_ b, this mutates b

Mutation of CreateFrom Mutates the Source Value

CreateFrom a <- b
Mutate a
=>
Mutate b

Example:

const a = b[index];
a.property = value // the contents of b are transitively mutated

Mutation of Capture Does Not Mutate the Source Value

Capture a <- b
Mutate a
!=>
~Mutate b~

Example:

const a = {};
a.b = b;
a.property = value; // mutates a, not b

Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture

Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
Mutate b
=>
Mutate a

A derived value changes when it's source value is mutated.

Example:

const x = {};
const y = [x];
x.y = true; // this changes the value within `y` ie mutates y

TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source

Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
MutateTransitive a
=>
MutateTransitive b

Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets .field = value. Given a function mutate() that does this, then:

const a = b; // assign
mutate(a); // clearly can transitively mutate b

const a = maybeIdentity(b); // alias
mutate(a); // clearly can transitively mutate b

const a = b[index]; // createfrom
mutate(a); // clearly can transitively mutate b

const a = {};
a.b = b; // capture
mutate(a); // can transitively mutate b

Freeze Does Not Freeze the Value

Freeze does not freeze the value itself:

Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
!=>
~Freeze x~

This means that subsequent mutations of the original value are valid:

Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Mutate x
=>
Mutate x (mutation is ok)

As well as mutations through other assignments/aliases/captures/createfroms of the original value:

Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
Mutate z
=>
Mutate x (mutation is ok)

Freeze Freezes The Reference

Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.

Conditional mutations of the reference are no-ops:

Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
(no mutation)

And known mutations of the reference are errors:

Create x
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
Freeze y
MutateConditional y
=>
MutateFrozen y error=...

Corollary: Transitivity of Assign/Alias/CreateFrom/Capture

A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.

In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:

const f = (x) => {
    const y = [x];  // capture y <- x
    const z = y[0]; // createfrom z <- y
    return z;     // assign return <- z
}
// <Effect> return <- x

In this example we can see that there should be some effect on f that tracks the flow of data from x into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.

Each of the effects is transitive with itself

Assign b <- a
Assign c <- b
=>
Assign c <- a
Alias b <- a
Alias c <- b
=>
Alias c <- a
CreateFrom b <- a
CreateFrom c <- b
=>
CreateFrom c <- a
Capture b <- a
Capture c <- b
=>
Capture c <- a

Alias > Assign

Assign b <- a
Alias c <- b
=>
Alias c <- a
Alias b <- a
Assign c <- b
=>
Alias c <- a

CreateFrom > Assign/Alias

Intuition:

CreateFrom b <- a
Alias c <- b OR Assign c <- b
=>
CreateFrom c <- a
Alias b <- a OR Assign b <- a
CreateFrom c <- b
=>
CreateFrom c <- a

Capture > Assign/Alias

Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.

Capture b <- a
Alias c <- b OR Assign c <- b
=>
Capture c <- a
Alias b <- a OR Assign b <- a
Capture c <- b
=>
Capture c <- a

Capture And CreateFrom

Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:

Capture then CreatFrom is equivalent to Alias: we have to assume that the result is the original value and that a local mutation of the result could mutate the original.

const b = [a]; // capture
const c = b[0]; // createfrom
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom 

We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.

Capture b <- a
CreateFrom c <- b
=>
Alias c <- a

Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:

const b = a[0]; // createfrom
const c = [b]; // capture
mutate(c); // does not mutate a, so the result must be Capture
CreateFrom b <- a
Capture c <- b
=>
Capture c <- a