Commit Graph
20415 Commits
Author SHA1 Message Date
Joe Savona dbd6d754af [compiler] Post/Pre-FixUpdate, try/catch, ConditionallyMutateIterator support
ghstack-source-id: 62efaf761a
Pull Request resolved: https://github.com/facebook/react/pull/33427
2025-06-09 11:11:30 -07:00
Joe Savona bc780793f7 [compiler] Further improve new range inference
Further refine the abstract interpretation for InferMutationAliasingRanges to account for forward data flow: Alias/Capture a -> b, mutate(a) => mutate(b) ie mutation affects aliases of the place being mutated, not just things that place aliased.

Fixes inference of which context vars of a function mutate, using the precise inference from the Range pass.

Adds MutateFrozen and MutateGlobal effects but they're not fully hooked up yet.

ghstack-source-id: 0d83b1afa9
Pull Request resolved: https://github.com/facebook/react/pull/33411
2025-06-09 11:11:27 -07:00
Joe Savona 996b3be844 [compiler] InferMutationAliasingRanges precisely models which values mutate when
It turns out that InferMutationAliasingRanges does need a fixpoint loop, but the approach is arguably simpler overall and more precise than the previous implementation. Like InferMutationAliasingEffects (which is the new InferReferenceEffects), we build an abstract model of the heap. But here we know what the effects are, so we can do abstract interpretation of the effects. Each abstract value stores a set of values that it has captured (for transitive mutation), while each variable keeps a set of values it may directly mutate (for assign/alias/capturefrom).

This means that at each mutation, we can mark _exactly_ the set of variables/values that are affected by that specific instruction. This means we can correctly infer that `mutate(b)` can't impact `a` here:

```
a = make();
b = make();
mutate(b); // when we interpret the mutation here, a isn't captured yet
b.a = a;
```

We will need to make this a fixpoint, but only if there are backedges in the CFG.

ghstack-source-id: 6fd1662469
Pull Request resolved: https://github.com/facebook/react/pull/33401
2025-06-09 11:11:23 -07:00
Joe Savona 9dbf05f570 [compiler] Fix mutable ranges for StoreContext
ghstack-source-id: 1d78b11730
Pull Request resolved: https://github.com/facebook/react/pull/33386
2025-06-09 11:11:20 -07:00
Joe Savona 277ee3e9e0 [compiler] Distinguish Alias/Assign effects
Alias was akward becuase it had a specific data flow / mutability relationship — direct mutations affect both sides - but also populated the output. We need the ability to express the aliasing dataflwo/mutability relationship w/o expressing actual assignment.

An example of this is that for an unknown method call `const z = x.y()`, we want to express the Alias-style mutability relationship btw z and x (Muate(z) => Mutate(x)), but we want to assume that `z !== x`.

This ends up being really clean, the places that should use Assign are very obvious (LoadLocal, StoreLocal) and most places keep using Alias.

ghstack-source-id: ae2b35b3f4
Pull Request resolved: https://github.com/facebook/react/pull/33385
2025-06-09 11:11:17 -07:00
Joe Savona 765e7f1151 [compiler] Effect inference across signatures and user-provided callbacks
This ties all the ideas together, showing the promise of the new inference. When applying effects, when we encounter an Apply effect we now check to see if the function we're calling is a locally defined FunctionExpression. If so, we construct a signature for it on the fly (we already have created the effects in AnalyzeFunctions), substitute in the args to get a set of effects we can apply, and then recursively apply those effects.

This required adding an ability for signatures to declare additional temporary places that they can reference. For example, Array.prototype.map needs a temporary to represent the items it extracts from the receiver array, and another temporary for the result of calling the user-provided function.

This also meant adding a `CreateFunction` effect which a) allows us to preserve the FunctionExpression value in the inference state (the previous Create effect meant we just created a dummy ObjectExpression) and b) allows dynamically constructing the ValueKind of the function based on whether it actually captures any mutable values.

Lots of other little fixes as well, such as changing function related effects (and PropertyLoad) to use Alias instead of Capture so that subsequent mutations of the output count as mutations of the input.

ghstack-source-id: 93c3bc3a0c
Pull Request resolved: https://github.com/facebook/react/pull/33384
2025-06-09 11:11:14 -07:00
Joe Savona 887c4f1839 [compiler] Receiver is mutate? for functions wo signatures
ghstack-source-id: 71bc9297af
Pull Request resolved: https://github.com/facebook/react/pull/33380
2025-06-09 11:11:10 -07:00
Joe Savona 029d4eac14 [compiler] Handle legacy mutableIfOperandsMutable signatures
ghstack-source-id: 3478c26d27
Pull Request resolved: https://github.com/facebook/react/pull/33379
2025-06-09 11:11:07 -07:00
Joe Savona 04abd518ee [compiler] Prep for making new/call/etc use Apply effects
ghstack-source-id: d7f94f3623
Pull Request resolved: https://github.com/facebook/react/pull/33378
2025-06-09 11:11:04 -07:00
Joe Savona 8ca284f58d [compiler] Bailout on mutations of frozen/global values
ghstack-source-id: ea86f072a1
Pull Request resolved: https://github.com/facebook/react/pull/33377
2025-06-09 11:11:01 -07:00
Joe Savona 61ef0959de [compiler] comments and todos
ghstack-source-id: 851743f0d6
Pull Request resolved: https://github.com/facebook/react/pull/33376
2025-06-09 11:10:58 -07:00
Joe Savona 6e8b6c98e7 [compiler] Translate legacy FunctionSignature into new AliasingEffects
To help bootstrap the new inference model, this PR adds a helper that takes a legacy FunctionSignature and converts into a list of (new) AliasingEffects. This conversion tries to make explicit all the implicit handling of InferReferenceEffects and previous FunctionSignature.

For example, the signature for Array.proto.pop has a calleeEffect of `Store`. Nowhere does it say that the receiver flows into the result! There's an implicit behavior that the receiver flows into the result. The new function makes this explicit by emitting a `Capture receiver -> lvalue` effect.

So far I confirmed that this works for Array.proto.push() if i hard code the inference to ignore new-style aliasing signatures. I'll continue to refine it going forward as I start running the new inference on more fixtures.

ghstack-source-id: ba2fec0e25
Pull Request resolved: https://github.com/facebook/react/pull/33371
2025-06-09 11:10:55 -07:00
Joe Savona 7265729d8d [compiler] First example of an aliasing signature (array push)
Adds an aliasing signature for Array.prototype.push and fixes up the logic for consuming these signatures during effect inference. As the test fixture shows, we correctly model the capturing. Mutable values that are captured into the array count as co-mutated if the array itself is transitively mutated later, while mutable values captured after such transitive mutation are not considered co-mutated.

The implementation is based on the fact that the final `push` call is only locally mutating the array. During the phase that looks at local mutations we are only tracking direct aliases, and the push doesn't directly alias the items to the array, it only captures them.

ghstack-source-id: 204e369686
Pull Request resolved: https://github.com/facebook/react/pull/33370
2025-06-09 11:10:53 -07:00
Joe Savona 84f8a0d65a [compiler] Delay mutation of function expr context variables until function is called
See comments in code. The idea is that rather than immediately processing function expression effects when declaring the function, we record Capture effects for context variables that may be captured/mutated in the function. Then, transitive mutations of the function value itself will extend the range of these values via the normal captured value comutation inference established earlier in the stack (if capture a -> b, then transitiveMutate(b) => mutate(a)). So capture contextVar -> function and transitiveMutate(function) => mutate(contextVar).

ghstack-source-id: 355895578e
Pull Request resolved: https://github.com/facebook/react/pull/33369
2025-06-09 11:10:50 -07:00
Joe Savona 04d0aa795a [compiler] Add ImmutableCapture effect, CreateFrom no longer needs Capture
ImmutableCapture allows us to record information flow for escape analysis purposes, without impacting mutable range analysis. All of Alias, Capture, and CreateFrom now downgrade to ImmutableCapture if the `from` value is frozen. Globals and primitives are conceptually copy types and don't record any information flow at all. I guess we could add a `Copy` effect if we wanted but i haven't seen a need for it yet.

Related, previously CreateFrom was always paired with Capture when constructing effects. But I had also coded up the inference to sort of treat them the same. So this diff makes that official, and means CreateFrom is a valid third way to represent data flow and not just creation. When applied, CreateFrom will turn into: ImmutableCapture for frozen values, Capture for mutable values, and disappear for globals/primitives.

A final tweak is to change range inference to ensure that range.start is non-zero if the value is mutated.

ghstack-source-id: fef144d7fb
Pull Request resolved: https://github.com/facebook/react/pull/33367
2025-06-09 11:10:47 -07:00
Joe Savona d291f98573 [compiler] Improve inference of function expression mutation/aliasing effects
Distinguish the various forms of mutation, and do basic replaying of these effects when the function is created (imperfect, temporary).

ghstack-source-id: c6d6d897a1
Pull Request resolved: https://github.com/facebook/react/pull/33365
2025-06-09 11:10:44 -07:00
Joe Savona d8eccc8f40 [compiler] Alternate pipeline for new mutability model
This PR gets a first fixture working end-to-end with the new mutability and aliasing model. Key changes:

* Add a feature flag to enable the model. When enabled we no longer call InferReferenceEffects or InferMutableRanges, and instead use the new equivalents.
* Adds a pass that infers Place-specific effects based on mutable ranges and instruction effects. This is necessary to satisfy existing code that requires operand effects to be populated.
* Adds a pass that infers the outwardly-visible capturing/aliasing behavior of a function expression. The idea is that this can bubble up and be used in conjunction with the `Apply` effect to get precise inference of things like `array.map(() => { ... })`.

ghstack-source-id: 03427439ec
Pull Request resolved: https://github.com/facebook/react/pull/33364
2025-06-09 11:10:40 -07:00
Joe Savona 7cfe507be8 [compiler] Add HIRFunction.returns: Place
ghstack-source-id: 274d5ff328
Pull Request resolved: https://github.com/facebook/react/pull/33363
2025-06-09 11:10:36 -07:00
Joe Savona 0688114941 [compiler] Foundation of new mutability and aliasing (alternate take)
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

ghstack-source-id: 5b9c88a5f1
Pull Request resolved: https://github.com/facebook/react/pull/33346
2025-06-09 11:10:33 -07:00
Joe Savona 77d33803e2 [compiler] Add Instruction.effects property
ghstack-source-id: ffdbd73de5
Pull Request resolved: https://github.com/facebook/react/pull/33350
2025-06-09 11:10:31 -07:00
Joe Savona 631644075f [compiler] Repro for imprecise memo due to closure capturing changes
Syncing this stack internally there is a small percentage of files that lose memoization, generally for callbacks. The repro here tries to get at the core pattern, where a parameter escapes into a mutable return value. This makes the callback appear mutable, and means that calls like array.map aren't able to optimize as well — even if the array itself is transitively immutable.

The challenge is that we can't really distinguish between just capturing and true mutation right now — AnalyzeFunctions kind of has to pick one, and consider both a mutation.

ghstack-source-id: 68cecb869f
Pull Request resolved: https://github.com/facebook/react/pull/33180
2025-06-09 11:10:28 -07:00
Joe Savona 114b4c1001 [compiler] avoid empty switch case bodies
ghstack-source-id: bec0ccd794
Pull Request resolved: https://github.com/facebook/react/pull/33179
2025-06-09 11:10:25 -07:00
Joe Savona 5fc694a282 [compiler] allow local fixtures to be excluded from git w "nocommit" prefix
ghstack-source-id: acd64f7eaf
Pull Request resolved: https://github.com/facebook/react/pull/33178
2025-06-09 11:10:21 -07:00
Joe Savona 741854820f [compiler] Fix for PropertyStore object effect
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

ghstack-source-id: fef41edcc5
Pull Request resolved: https://github.com/facebook/react/pull/33164
2025-06-09 11:10:18 -07:00
Joe Savona ac2c71e44d [compiler] Fixture tests for PropertyStore effects
Adds fixture tests to demonstrate an issue in changing PropertyStore to always have a Store effect on its object operand, regardless of the operand type. The issue is that if we're doing a PropertyStore on a nested value, that has be considered a transitive mutation of the parent object:

```
const x = {y: {z: {}}};
x.y.z.key = 'value'; // this has to be a mutation of `x`
```

Fix in the next PR.

ghstack-source-id: 4491c86970
Pull Request resolved: https://github.com/facebook/react/pull/33163
2025-06-09 11:10:15 -07:00
Joe Savona 17ffa32fe9 [compiler] Move co-mutation range extension to InferMutableRanges
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

ghstack-source-id: 829d114d12
Pull Request resolved: https://github.com/facebook/react/pull/33157
2025-06-09 11:10:11 -07:00
Joe Savona 01f0449b1e [compiler][wip] Infer alias effects for function expressions
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

As part of this, I realized that our handling of PropertyStore's effect wasn't quite right. We used a store effect for the object, but only if it was a known object type — otherwise we recorded it as a mutation. But a PropertyStore really always is a store — it only mutates direct aliases of a value, not any interior objects that are captured. So I updated to always use store for known properties, and use mutate for computed properties. The latter is still also wrong, but i want to debug the change there separately.

ghstack-source-id: 32978ceda6
Pull Request resolved: https://github.com/facebook/react/pull/33151
2025-06-09 11:10:08 -07:00
Joe Savona ff77c0abe9 [compiler] Correctly infer context mutation places as outer (context) places
The issue in the previous PR was due to a ContextMutation function effect having a place that wasn't one of the functions' context variables. What was happening is that the `getContextRefOperand()` helper wasn't following aliases. If an operand had a context type, we recorded the operand as the context place — but instead we should be looking through to the context places of the abstract value.

With this change the fixture now fails for a different reason — we infer this as a mutation of `params` and reject it because `params` is frozen (hook return value). This case is clearly a false positive: the mutation is on the outer, new `nextParams` object and can't possibly mutate `params`. Need to think more about what to do here but this is clearly more precise in terms of which variable we record as the context variable.

ghstack-source-id: 37bda1509b
Pull Request resolved: https://github.com/facebook/react/pull/33114
2025-06-09 11:10:05 -07:00
Joe Savona 34dfede6ed [compiler] Repro for false positive ValidateNoFreezingKnownMutableFunctions
Found when testing the new validation from #33079 internally. I haven't fully debugged, but somehow the combination of the effect function *accessing* a ref and also calling a second function which has a purely local mutation triggers the validation. Even though the called second function only mutates local variables. If i remove the ref access in the effect function, the error goes away.

Anyway I'll keep debugging, putting up a repro for now.

ghstack-source-id: f22345952a
Pull Request resolved: https://github.com/facebook/react/pull/33113
2025-06-09 11:10:02 -07:00
5dc1b212c3 [Fizz] Support basic SuspenseList forwards/backwards revealOrder (#33306)
Basically we track a `SuspenseListRow` on the task. These keep track of
"pending tasks" that block the row. A row is blocked by:

- First itself completing rendering.
- A previous row completing.
- Any tasks inside the row and before the Suspense boundary inside the
row. This is mainly because we don't yet know if we'll discover more
SuspenseBoundaries.
- Previous row's SuspenseBoundaries completing.

If a boundary might get outlined, then we can't consider it completed
until we have written it because it determined whether other future
boundaries in the row can finish.

This is just handling basic semantics. Features not supported yet that
need follow ups later:

- CSS dependencies of previous rows should be added as dependencies of
future row's suspense boundary. Because otherwise if the client is
blocked on CSS then a previous row could be blocked but the server
doesn't know it.
- I need a second pass on nested SuspenseList semantics.
- `revealOrder="together"`
- `tail="hidden"`/`tail="collapsed"`. This needs some new runtime
semantics to the Fizz runtime and to allow the hydration to handle
missing rows in the HTML. This should also be future compatible with
AsyncIterable where we don't know how many rows upfront.
- Need to double check resuming semantics.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-05-19 15:16:42 -04:00
Jan KassensandGitHub a3abf5f2f8 [eslint-plugin-react-hooks] add experimental_autoDependenciesHooks option (#33294) 2025-05-19 15:08:30 -04:00
Sebastian MarkbågeandGitHub 462d08f9ba Move SuspenseListProps into a shared/ReactTypes (#33298)
So they can be shared by server. Incorporates the types from definitely
typed too.
2025-05-17 20:00:56 -04:00
Sebastian MarkbågeandGitHub 6060367ef8 [Fizz] Wrap revealCompletedBoundaries in a ViewTransitions aware version (#33293)
When needed.

For the external runtime we always include this wrapper.

For others, we only include it if we have an ViewTransitions affecting.
If we discover the ViewTransitions late, then we can upgrade an already
emitted instruction.

This doesn't yet do anything useful with it, that's coming in a follow
up. This is just the mechanism for how it gets installed.
2025-05-17 18:18:24 -04:00
Sebastian MarkbågeandGitHub c250b7d980 [Fizz] Should be considered complete inside onShellReady callback (#33295)
We decremented `allPendingTasks` after invoking `onShellReady`. Which
means that in that scope it wasn't considered fully complete.

Since the pattern for flushing in Node.js is to start piping in
`onShellReady` and that's how you can get sync behavior, this led us to
think that we had more work left to do. For example we emitted the
`writeShellTimeInstruction` in this scenario before.
2025-05-16 14:53:40 -04:00
Jan KassensandGitHub 4448b18760 [eslint-plugin-react-hooks] fix exhaustive deps lint rule with component syntax (#33182) 2025-05-15 12:51:18 -04:00
RickyandGitHub 4a45ba92c4 [sync] Fix noop for xplat (#33214)
Noop detection for xplat syncs broke because `eslint-plugin-react-hooks`
uses versions like:

- `0.0.0-experimental-d85f86cf-20250514`

But xplat expects them to be of the form:

- `19.2.0-native-fb-63d664b2-20250514`

This PR fixes the noop by ignoring
`eslint-plugin-react-hooks/package.json` changes. This means we won't
create a sync if only that package.json changes, but that should be rare
and we can follow up with better detection if needed.

[Example failed
action](https://github.com/facebook/react/actions/runs/15032346805/job/42247414406):

<img width="1031" alt="Screenshot 2025-05-15 at 11 31 17 AM"
src="https://github.com/user-attachments/assets/d902079c-1afe-4e18-af1d-25e60e28929e"
/>

I believe the regression was caused by
https://github.com/facebook/react/pull/33104
2025-05-15 12:12:51 -04:00
laurenandGitHub 08cb2d7ee7 [ci] Log author_association (#33213)
For debugging purposes, log author_association
2025-05-15 11:49:56 -04:00
laurenandGitHub 203df2c940 [compiler] Update changelog for 19.1.0-rc.2 (#33207)
Update the changelog.
2025-05-15 10:34:11 -04:00
Sebastian MarkbågeandGitHub 65b5aae010 [Fizz] Add vt- prefix attributes to annotate <ViewTransition> in HTML (#33206)
Stacked on #33194 and #33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.
2025-05-15 01:04:10 -04:00
Sebastian MarkbågeandGitHub 3f67d0857e [Fizz] Track whether we're in a fallback on FormatContext (#33194)
Removes the `isFallback` flag on Tasks and tracks it on the
formatContext instead.

Less memory and avoids passing and tracking extra arguments to all the
pushStartInstance branches that doesn't need it.

We'll need to be able to track more Suspense related contexts on this
for View Transitions anyway.
2025-05-15 00:06:06 -04:00
Sebastian MarkbågeandGitHub 96eb84e493 Claim the useId name space for every auto named ViewTransition (#33200)
This is a partial revert of #33094. It's true that we don't need the
server and client ViewTransition names to line up. However the server
does need to be able to generate deterministic names for itself. The
cheapest way to do that is using the useId algorithm. When it's used by
the server, the client needs to also materialize an ID even if it
doesn't use it.
2025-05-14 17:52:41 -04:00
Sebastian MarkbågeandGitHub 63d664b220 Don't consider Portals animating unless they're wrapped in a ViewTransition (#33191)
And that doesn't disable with `update="none"`.

The principle here is that we want the content of a Portal to animate if
other things are animating with it but if other things aren't animating
then we don't.
2025-05-14 17:50:56 -04:00
Jan KassensandGitHub d85f86cf01 Delete stray file (#33199)
Not sure where this was coming from.
2025-05-14 11:27:36 -04:00
Sebastian MarkbågeandGitHub 3a5b326d81 [Fiber] Trigger default indicator for isomorphic async actions with no root associated (#33190)
Stacked on #33160, #33162, #33186 and #33188.

We have a special case that's awkward for default indicators. When you
start a new async Transition from `React.startTransition` then there's
not yet any associated root with the Transition because you haven't
necessarily `setState` on anything yet until the promise resolves.
That's what `entangleAsyncAction` handles by creating a lane that
everything entangles with until all async actions are done.

If there are no sync updates before the end of the event, we should
trigger a default indicator until either the async action completes
without update or if it gets entangled with some roots we should keep it
going until those roots are done.
2025-05-13 16:10:28 -04:00
Sebastian MarkbågeandGitHub 59440424d0 Implement Navigation API backed default indicator for DOM renderer (#33162)
Stacked on #33160.

By default, if `onDefaultTransitionIndicator` is not overridden, this
will trigger a fake Navigation event using the Navigation API. This is
intercepted to create an on-going navigation until we complete the
Transition. Basically each default Transition is simulated as a
Navigation.

This triggers the native browser loading state (in Chrome at least). So
now by default the browser spinner spins during a Transition if no other
loading state is provided. Firefox and Safari hasn't shipped Navigation
API yet and even in the flag Safari has, it doesn't actually trigger the
native loading state.

To ensures that you can still use other Navigations concurrently, we
don't start our fake Navigation if there's one on-going already.
Similarly if our fake Navigation gets interrupted by another. We wait
for on-going ones to finish and then start a new fake one if we're
supposed to be still pending.

There might be other routers on the page that might listen to intercept
Navigation Events. Typically you'd expect them not to trigger a refetch
when navigating to the same state. However, if they want to detect this
we provide the `"react-transition"` string in the `info` field for this
purpose.
2025-05-13 16:00:38 -04:00
Sebastian MarkbågeandGitHub b480865db0 [Fiber] Always flush Default priority in the microtask if a Transition was scheduled (#33186)
Stacked on #33160.

The purpose of this is to avoid calling `onDefaultTransitionIndicator`
when a Default priority update acts as the loading indicator, but still
call it when unrelated Default updates happens nearby.

When we schedule Default priority work that gets batched with other
events in the same frame more or less. This helps optimize by doing less
work. However, that batching means that we can't separate work from one
setState from another. If we would consider all Default priority work in
a frame when determining whether to show the default we might never show
it in cases like when you have a recurring timer updating something.

This instead flushes the Default priority work eagerly along with the
sync work at the end of the event, if this event scheduled any
Transition work. This is then used to determine if the default indicator
needs to be shown.
2025-05-13 15:52:44 -04:00
Sebastian MarkbågeandGitHub 62d3f36ea7 [Fiber] Trigger default transition indicator if needed (#33160)
Stacked on #33159.

This implements `onDefaultTransitionIndicator`.

The sequence is:

1) In `markRootUpdated` we schedule Transition updates as needing
`indicatorLanes` on the root. This tracks the lanes that currently need
an indicator to either start or remain going until this lane commits.
2) Track mutations during any commit. We use the same hook that view
transitions use here but instead of tracking it just per view transition
scope, we also track a global boolean for the whole root.
3) If a sync/default commit had any mutations, then we clear the
indicator lane for the `currentEventTransitionLane`. This requires that
the lane is still active while we do these commits. See #33159. In other
words, a sync update gets associated with the current transition and it
is assumed to be rendering the loading state for that corresponding
transition so we don't need a default indicator for this lane.
4) At the end of `processRootScheduleInMicrotask`, right before we're
about to enter a new "event transition lane" scope, it is no longer
possible to render any more loading states for the current transition
lane. That's when we invoke `onDefaultTransitionIndicator` for any roots
that have new indicator lanes.
5) When we commit, we remove the finished lanes from `indicatorLanes`
and once that reaches zero again, then we can clean up the default
indicator. This approach means that you can start multiple different
transitions while an indicator is still going but it won't stop/restart
each time. Instead, it'll wait until all are done before stopping.

Follow ups:

- [x] Default updates are currently not enough to cancel because those
aren't flush in the same microtask. That's unfortunate. #33186
- [x] Handle async actions before the setState. Since these don't
necessarily have a root this is tricky. #33190
- [x] Disable for `useDeferredValue`. ~Since it also goes through
`markRootUpdated` and schedules a Transition lane it'll get a default
indicator even though it probably shouldn't have one.~ EDIT: Turns out
this just works because it doesn't go through `markRootUpdated` when
work is left behind.
- [x] Implement built-in DOM version by default. #33162
2025-05-13 15:45:11 -04:00
Sebastian MarkbågeandGitHub 0cac32d60d [Fiber] Stash the entangled async action lane on currentEventTransitionLane (#33188)
When we're entangled with an async action lane we use that lane instead
of the currentEventTransitionLane. Conversely, if we start a new async
action lane we reuse the currentEventTransitionLane.

So they're basically supposed to be in sync but they're not if you
resolve the async action and then schedule new stuff in the same event.
Then you end up with two transitions in the same event with different
lanes.

By stashing it like this we fix that but it also gives us an opportunity
to check just the currentEventTransitionLane to see if this event
scheduled any regular Transition updates or Async Transitions.
2025-05-13 15:20:59 -04:00
Sebastian MarkbågeandGitHub 676f0879f3 Reset currentEventTransitionLane after flushing sync work (#33159)
This keeps track of the transition lane allocated for this event. I want
to be able to use the current one within sync work flushing to know
which lane needs its loading indicator cleared.

It's also a bit weird that transition work scheduled inside sync updates
in the same event aren't entangled with other transitions in that event
when `flushSync` is.

Therefore this moves it to reset after flushing.

It should have no impact. Just splitting it out into a separate PR for
an abundance of caution.

The only thing this might affect would be if the React internals throws
and it doesn't reset after. But really it doesn't really have to reset
and they're all entangled anyway.
2025-05-13 15:18:02 -04:00
Sebastian MarkbågeandGitHub 997c7bc930 [DevTools] Get source location from structured callsites in prepareStackTrace (#33143)
When we get the source location for "View source for this element" we
should be using the enclosing function of the callsite of the child. So
that we don't just point to some random line within the component.

This is similar to the technique in #33136.

This technique is now really better than the fake throw technique, when
available. So I now favor the owner technique. The only problem it's
only available in DEV and only if it has a child that's owned (and not
filtered).

We could implement this same technique for the error that's thrown in
the fake throwing solution. However, we really shouldn't need that at
all because for client components we should be able to call
`inspect(fn)` at least in Chrome which is even better.
2025-05-13 12:39:10 -04:00