Commit Graph

463 Commits

Author SHA1 Message Date
Sebastian "Sebbie" Silbermann
79ca5ae855
Bump next prerelease version numbers (#34674) 2025-10-02 00:31:55 +02:00
Sebastian "Sebbie" Silbermann
861811347b
Bump scheduler version (#34671)
The canaries have been published depending on 0.27-canary. Bumping
scheduler just in case to be sure.
2025-10-01 22:45:31 +02:00
Sebastian "Sebbie" Silbermann
5667a41fe4
Bump next prerelease version numbers (#34639) 2025-10-01 15:15:24 +02:00
Sebastian Markbåge
e233218359
Track "Animating" Entry for Gestures while the Gesture is Still On-going (#34548)
Stacked on #34546.

Same as #34538 but for gestures.

Includes various fixes.

This shows how it ends with a Transition when you release in the
committed state. Note how the Animation of the Gesture continues until
the Transition is done so that the handoff is seamless.

<img width="853" height="134" alt="Screenshot 2025-09-20 at 7 37 29 PM"
src="https://github.com/user-attachments/assets/6192a033-4bec-43b9-884b-77e3a6f00da6"
/>
2025-09-24 11:26:03 -04:00
Sebastian Markbåge
b4fe1e6c7e
Log the time until the Animation finishes as "Animating" (#34538)
Stacked on #34522.

<img width="1025" height="200" alt="Screenshot 2025-09-19 at 6 37 28 PM"
src="https://github.com/user-attachments/assets/f25900f6-6503-48b1-876d-bd6697a29c6f"
/>

We already cover the time between "Starting Animation" and "Remaining
Effects" as "Animating". However, if the effects are forced then we can
still be animating after that. This fills in that gap.

This also fills in the gap if another render starts before the animation
finishes on the same track. It'll mark the blank space between the
previous render finishing and the next render starting as "Animating".

This should correspond roughly to the native "Animations" track.
2025-09-20 11:10:42 -04:00
Sebastian Markbåge
b204edda3a
Log Custom Reason for the Suspended Commit Track (#34522)
Stacked on #34511.

We currently log all Suspended Commit as "Suspended on Images or CSS"
but it can really be other reasons too now. Like waiting on the previous
View Transition. This allows the host config configure this reason.

Now when one animation starts before another one finishes we log that as
"Waiting for the previous Animation".

<img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM"
src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3"
/>
2025-09-20 11:01:52 -04:00
Sebastian Markbåge
ad578aa01f
Log Suspended startViewTransition Phase (#34511)
Stacked on #34510.

The "Commit" phase for a View Transition starts before the snapshot
phase (before mutation) and then stretches into the async gap of
`startViewTransition`, encompasses the mutation phase inside of its
update callback and finally the layout phase.

However, between the mutation phase and the layout phase we may suspend
the start of the view transition on fonts and/or images. In that case we
now split the Commit phase into first one before we suspend and then we
log "Waiting for Images and/or Fonts" and then another Commit phase
around the layout effects.

<img width="897" height="119" alt="Screenshot 2025-09-16 at 11 37 26 PM"
src="https://github.com/user-attachments/assets/0fe21388-bb48-4456-a594-62227d12d9b7"
/>
2025-09-18 15:25:41 -04:00
Sebastian Markbåge
5d49b2b7f4
[Fiber] Track SuspendedState on stack instead of global (#34486)
Stacked on #34481.

We currently track the suspended state temporarily with a global which
is safe as long as we always read it during a sync pass. However, we
sometimes read it in closures and then we have to be carefully to pass
the right one since it's possible another commit on a different root has
started at that point. This avoids this footgun.

Another reason to do this is that I want to read it in
`startViewTransition` which is in an async gap after which point it's no
longer safe. So I have to pass that through the `commitRoot` bound
function.
2025-09-15 16:10:47 -04:00
Sebastian Markbåge
e3f191803c
[Fiber] Adjust the suspensey image/css timeout based on already elapsed time (#34478)
Currently suspensey images doesn't account for how long we've already
been waiting. This means that you can for example wait for 300ms for the
throttle + 500ms for the images. If a Transition takes a while to
complete you can also wait that time + an additional 500ms for the
images.

This tracks the start time of a Transition so that we can count the
timeout starting from when the user interacted or when the last fallback
committed (which is where the 300ms throttle is computed from). Creating
a single timeline.

This also moves the timeout to a central place which I'll use in a
follow up.
2025-09-15 16:05:20 -04:00
Jan Kassens
4049cfeeab
Update Flow to 0.273 (#34274)
This version introduces "Natural Inference" which requires a couple more
type annotations to make Flow pass.
2025-08-22 16:58:01 -04:00
Jan Kassens
6de32a5a07
Update Flow to 0.263 (#34269)
This update was a bit more involved.

- `React$Component` was removed, I replaced it with Flow component
types.
- Flow removed shipping the standard library. This adds the environment
libraries back from `flow-typed` which seemed to have changed slightly
(probably got more precise and less `any`s). Suppresses some new type
errors.
2025-08-22 12:10:13 -04:00
Jan Kassens
ec5dd0ab3a
Update Flow to 0.257 (#34253)
After an easy couple version with #34252, this version is less flexible
(and safer) on inferring exported types mainly.

We require to annotate some exported types to differentiate between
`boolean` and literal `true` types, etc.
2025-08-21 13:30:01 -04:00
Sebastian Markbåge
21fdf308a1
Use a shared noop function from shared/noop (#33154)
Stacked on #33150.

We use `noop` functions in a lot of places as place holders. I don't
think there's any real optimizations we get from having separate
instances. This moves them to use a common instance in `shared/noop`.
2025-05-08 21:33:18 -04:00
Sebastian Markbåge
9b79292ae7
Add plumbing for onDefaultTransitionIndicator (#33150)
This just adds the options at the root and wire it up to the root but it
doesn't do anything yet.
2025-05-08 20:42:50 -04:00
Sebastian Markbåge
efb22d8850
Add Suspensey Images behind a Flag (#32819)
We've known we've wanted this for many years and most of the
implementation was already done for Suspensey CSS. This waits to commit
until images have decoded by default or up to 500ms timeout (same as
suspensey fonts).

It only applies to Transitions, Retries (Suspense), Gesture Transitions
(flag) and Idle (doesn't exist). Sync updates just commit immediately.

`<img loading="lazy" src="..." />` opts out since you explicitly want it
to load lazily in that case.

`<img onLoad={...} src="..." />` also opts out since that implies you're
ok with managing your own reveal.

In the future, we may add an opt in e.g. `<img blocking="render"
src="..." />` that opts into longer timeouts and re-suspends even sync
updates. Perhaps also triggering error boundaries on errors.

The rollout for this would have to go in a major and we may have to
relax the default timeout to not delay too much by default. However, we
can also make this part of `enableViewTransition` so that if you opt-in
by using View Transitions then those animations will suspend on images.
That we could ship in a minor.
2025-04-04 14:54:05 -04:00
Sebastian Markbåge
0b1a9e90c5
Support addTransitionType in startGestureTransition (#32792)
Stacked on #32788.

Normally we track `addTransitionType` globally because of the async gap
that can happen in Actions where we lack AsyncContext to associate it
with a particular Transition. This unfortunately also means it's
possible to call outside of `startTransition` which is something we want
to warn for.

We need to be able to distinguish whether `addTransitionType` is for a
regular Transition or a Gesture Transition though.

Since `startGestureTransition` is only synchronous we can track it
within that execution scope and move it to a separate set. Since we know
for sure which call owns it we can properly associate it with that
specific provider's `ScheduledGesture`.

This does not yet handle calling `addTransitionType` inside the render
phase of a gesture. That would currently still be associated with the
next Transition instead.
2025-04-01 12:08:55 -04:00
Sebastian Markbåge
0a7cf20b22
Remove useSwipeTransition (#32786)
Stacked on #32785.

This is now replaced by `startGestureTransition` added in #32785.

I also renamed the flag from `enableSwipeTransition` to
`enableGestureTransition` to correspond to the new name.
2025-04-01 11:43:33 -04:00
lauren
50c5cdb653
Bump next prerelease version numbers (#32782)
Updates the version numbers in the prerelease channels.
2025-03-28 16:20:04 -04:00
Sebastian Markbåge
a5297ece62
Don't flush synchronous work if we're in the middle of a ViewTransition async sequence (#32760)
Starting a View Transition is an async sequence. Since React can get a
sync update in the middle of sequence we sometimes interrupt that
sequence.

Currently, we don't actually cancel the View Transition so it can just
run as a partial. This ensures that we fully skip it when that happens,
as well as warn.

However, it's very easy to trigger this with just a setState in
useLayoutEffect right now. Therefore if we're inside the preparing
sequence of a startViewTransition, this delays work that would've
normally flushed in a microtask. ~Maybe we want to do the same for
Default work already scheduled through a scheduler Task.~ Edit: This was
already done.

`flushSync` currently will still lead to an interrupted View Transition
(with a warning). There's a tradeoff here whether we want to try our
best to preserve the guarantees of `flushSync` or favor the animation.
It's already possible to suspend at the root with `flushSync` which
means it's not always 100% guaranteed to commit anyway. We could treat
it as suspended. But let's see how much this is a problem in practice.
2025-03-26 14:40:23 -04:00
Sebastian Markbåge
3c3696d554
Measure Updated ViewTransition Boundaries (#32653)
This does the same thing for `measureUpdateViewTransition` that we did
for `measureNestedViewTransitions` in
e3cbaffef0.
If a boundary hasn't mutated and didn't change in size, we mark it for
cancellation. Otherwise we add names to it. The different from the
CommitViewTransition path is that the "old" names are added to the
clones so this is the first time the "new" names.

Now we also cancel any boundaries that were unchanged. So now the root
no longer animates. We still have to clone them. There are other
optimizations that can avoid cloning but once we've done all the layouts
we can still cancel the running animation and let them just be the
regular content if they didn't change. Just like the regular
fire-and-forget path.

This also fixes the measurement so that we measure clones by adjusting
their position back into the viewport.

This actually surfaces a bug in Safari that was already in #32612. It
turns out that the old names aren't picked up for some reason and so in
Safari they looked more like a cross-fade than what #32612 was supposed
to fix. However, now that bug is even more apparent because they
actually just disappear in Safari. I'm not sure what that bug is but
it's unrelated to this PR so will fix that separately.
2025-03-17 21:38:13 -04:00
Jack Pope
6aa8254bb7
Add ref to Fragment (#32465)
*This API is experimental and subject to change or removal.*

This PR is an alternative to
https://github.com/facebook/react/pull/32421 based on feedback:
https://github.com/facebook/react/pull/32421#pullrequestreview-2625382015
. The difference here is that we traverse from the Fragment's fiber at
operation time instead of keeping a set of children on the
`FragmentInstance`. We still need to handle newly added or removed child
nodes to apply event listeners and observers, so we treat those updates
as effects.

**Fragment Refs**

This PR extends React's Fragment component to accept a `ref` prop. The
Fragment's ref will attach to a custom host instance, which will provide
an Element-like API for working with the Fragment's host parent and host
children.

Here I've implemented `addEventListener`, `removeEventListener`, and
`focus` to get started but we'll be iterating on this by adding
additional APIs in future PRs. This sets up the mechanism to attach refs
and perform operations on children. The FragmentInstance is implemented
in `react-dom` here but is planned for Fabric as well.

The API works by targeting the first level of host children and proxying
Element-like APIs to allow developers to manage groups of elements or
elements that cannot be easily accessed such as from a third-party
library or deep in a tree of Functional Component wrappers.

```javascript
import {Fragment, useRef} from 'react';

const fragmentRef = useRef(null);

<Fragment ref={fragmentRef}>
  <div id="A" />
  <Wrapper>
    <div id="B">
      <div id="C" />
    </div>
  </Wrapper>
  <div id="D" />
</Fragment>
```

In this case, calling `fragmentRef.current.addEventListener()` would
apply an event listener to `A`, `B`, and `D`. `C` is skipped because it
is nested under the first level of Host Component. If another Host
Component was appended as a sibling to `A`, `B`, or `D`, the event
listener would be applied to that element as well and any other APIs
would also affect the newly added child.

This is an implementation of the basic feature as a starting point for
feedback and further iteration.
2025-03-12 10:32:11 -04:00
Sebastian Markbåge
ca8f91f6f6
Log errors from startViewTransition to onRecoverableError (#32540)
We customize the messages only in DEV to keep it small in prod.

We skip some messages that are not really errors but more like
information.
2025-03-11 23:02:45 -04:00
Sebastian Markbåge
e9252bcdcc
During a Swipe Gesture Render a Clone Offscreen and Animate it Onscreen (#32500)
This is really the essence mechanism of the `useSwipeTransition`
feature.

We don't want to immediately switch to the destination state when
starting a gesture. The effects remain mounted on the current state. We
want the current state to be "live". This is important to for example
allow a video to keeping playing while starting a swipe (think
TikTok/Reels) and not stop until you've committed the action. The only
thing that can be live is the "new" state. Therefore we treat the
destination as the "old" state and perform a reverse animation from
there.

Ideally we could apply the old state to the DOM tree, take a snapshot
and then revert it back in the mutation of `startViewTransition`.
Unfortunately, the way `startViewTransition` was designed it always
paints one frame of the "old" state which would lead this to cause a
flicker.

To work around this, we need to create a clone of any View Transition
boundary that might be mutated and then render that offscreen. That way
we can render the "current" state on screen and the "destination" state
offscreen for the screenshots. Being mutated can be either due to React
doing a DOM mutation or if a child boundary resizes that causes the
parent to relayout. We don't have to do this for insertions or deletions
since they only appear on one side.

The worst case scenario is that we have to clone the whole root. That's
what this first PR implements. We clone the container and if it's not
absolutely positioned, we position it on top of the current one. If the
container is `document` or `<html>` we instead clone the `<body>` tag
since it's the only one we can insert a duplicate of. If the container
is deep in the tree we clone just that even though technically we should
probably clone the whole document in that case. We just keep the impact
smaller. Ideally though we'd never hit this case. In fact, if we clone
the document we issue a warning (always for now) since you probably
should optimize this. In the future I intend to add optimizations when
affected View Transition boundaries are absolutely positioned since they
cannot possibly relayout the parent. This would be the ideal way to use
this feature most efficiently but it still works without it.

Since we render the "old" state outside the viewport, we need to then
adjust the animation to put it back into the viewport. This is the
trickiest part to get right while still preserving any customization of
the View Transitions done using CSS. This current approach reapplies all
the animations with adjusted keyframes.

In the case of an "exit" the pseudo-element itself is positioned outside
the viewport but since we can't programmatically update the style of the
pseudo-element itself we instead adjust all the keyframes to put it back
into the viewport. If there is no animation on the group we add one.

In the case of an "update" the pseudo-element is positioned on the new
state which is already inside the viewport. However, the auto-generated
animation of the group has a starting keyframe that starts outside the
viewport. In this case we need to adjust that keyframe.

In the future I might explore a technique that inserts stylesheets
instead of mutating the animations. It might be simpler. But whatever
hacks work to maximize the compatibility is best.
2025-03-04 20:10:08 -05:00
Sebastian Markbåge
3607f4838a
Add Commit Scaffolding for Gestures (#32451)
This adds a `ReactFiberApplyGesture` which is basically intended to be a
fork of the phases in `ReactFiberCommitWork` except for the fake commit
that `useSwipeTransition` does. So far none of the phases are actually
implemented yet. This is just the scaffolding around them so I can fill
them in later.

The important bit is that we call `startViewTransition` (via the
`startGestureTransition` Config) when a gesture starts. We add a paused
animation to prevent the transition from committing (even if the
ScrollTimeline goes to 100%). This also locks the documents so that we
can't commit any other Transitions until it completes.

When the gesture completes (scroll end) then we stop the gesture View
Transition. If there's no new work scheduled we do that immediately but
if there was any new work already scheduled, then we assume that this
will potentially commit the new state. So we wait for that to finish.
This lets us lock the animation in its state instead of snapping back
and then applying the real update.

Using this technique we can't actually run a View Transition from the
current state to the actual committed state because it would snap back
to the beginning and then run the View Transition from there. Therefore
any new commit needs to skip View Transitions even if it should've
technically animated to that state. We assume that the new state is the
same as the optimistic state you already swiped to. An alternative to
this technique could be to commit the optimistic state when we cancel
and then apply any new updates o top of that. I might explore that in
the future.

Regardless it's important that the `action` associated with the swipe
schedules some work before we cancel. Otherwise it risks reverting
first. So I had to update this in the fixture.
2025-02-27 16:45:18 -05:00
Sebastian Markbåge
662957cc73
Allow passing range option to useSwipeTransition (#32412)
Stacked on #32379

Track the range offsets along the timeline where previous/current/next
is. This can also be specified as an option. This lets you model more
than three states along a timeline by clamping them and then updating
the "current" as you go.

It also allows specifying the "current" offset as something different
than what it was when the gesture started such as if it has to start
after scroll has already happened (such as what happens if you listen to
the "scroll" event).
2025-02-21 11:03:04 -05:00
Sebastian Markbåge
88479c6fc3
Rerender useSwipeTransition when direction changes (#32379)
We can only render one direction at a time with View Transitions. When
the direction changes we need to do another render in the new direction
(returning previous or next).

To determine direction we store the position we started at and anything
moving to a lower value (left/up) is "previous" direction (`false`) and
anything else is "next" (`true`) direction.

For the very first render we won't know which direction you're going
since you're still on the initial position. It's useful to start the
render to allow the view transition to take control before anything
shifts around so we start from the original position. This is not
guaranteed though if the render suspends.

For now we start the first render by guessing the direction such as if
we know that prev/next are the same as current. With the upcoming auto
start mode we can guess more accurately there before we start. We can
also add explicit APIs to `startGesture` but ideally it wouldn't matter.
Ideally we could just start after the first change in direction from the
starting point.
2025-02-20 18:13:09 -05:00
Sebastian Markbåge
028c8e6cf5
Add Transition Types (#32105)
This adds an isomorphic API to add Transition Types, which represent the
cause, to the current Transition. This is currently mainly for View
Transitions but as a concept it's broader and we might expand it to more
features and object types in the future.

```js
import { unstable_addTransitionType as addTransitionType } from 'react';

startTransition(() => {
  addTransitionType('my-transition-type');
  setState(...);
});
```

If multiple transitions get entangled this is additive and all
Transition Types are collected. You can also add more than one type to a
Transition (hence the `add` prefix).

Transition Types are reset after each commit. Meaning that `<Suspense>`
revealing after a `startTransition` does not get any View Transition
types associated with it.

Note that the scoping rules for this is a little "wrong" in this
implementation. Ideally it would be scoped to the nearest outer
`startTransition` and grouped with any `setState` inside of it.
Including Actions. However, since we currently don't have AsyncContext
on the client, it would be too easy to drop a Transition Type if there
were no other `setState` in the same `await` task. Multiple Transitions
are entangled together anyway right now as a result. So this just tracks
a global of all pending Transition Types for the next Transition. An
inherent tricky bit with this API is that you could update multiple
roots. In that case it should ideally be associated with each root.
Transition Tracing solves this by associating a Transition with any
updates that are later collected but this suffers from the problem
mentioned above. Therefore, I just associate Transition Types with one
root - the first one to commit. Since the View Transitions across roots
are sequential anyway it kind of makes sense that only one really is the
cause and the other one is subsequent.

Transition Types can be used to apply different animations based on what
caused the Transition. You have three different ways to choose from for
how to use them:

## CSS

It integrates with [View Transition
Types](https://www.w3.org/TR/css-view-transitions-2/#active-view-transition-pseudo-examples)
so you can match different animations based on CSS scopes:

```css
:root:active-view-transition-type(my-transition-type) {
  &::view-transition-...(...) {
    ...
  }
}
```

This is kind of a PITA to write though and if you have a CSS library
that provide View Transition Classes it's difficult to import those into
these scopes.

## Class per Type

This PR also adds an object-as-map form that can be passed to all
`className` properties:

```js
<ViewTransition className={{
  'my-navigation-type': 'hello',
  'default': 'world',
}}>
```

If multiple types match, then they're joined together. If no types match
then the special `"default"` entry is used instead. If any type has the
value `"none"` then that wins and the ViewTransition is disabled (not
assigned a name).

These can be combined with `enter`/`exit`/`update`/`layout`/`share`
props to match based on kind of trigger and Transition Type.

```js
<ViewTransition enter={{
  'navigation-back': 'enter-right',
  'navigation-forward': 'enter-left',
}}
exit={{
  'navigation-back': 'exit-right',
  'navigation-forward': 'exit-left',
}}>
```

## Events

In addition, you can also observe the types in the View Transition Event
callbacks as the second argument. That way you can pick different
imperative Animations based on the cause.

```js
<ViewTransition onUpdate={(inst, types) => {
  if (types.includes('navigation-back')) {
    ...
  } else if (types.includes('navigation-forward')) {
    ...
  } else {
    ...
  }
}}>
```

## Future

In the future we might expose types to `useEffect` for more general
purpose usage. This would also allow non-View Transition based
Animations such as existing libraries to use this same feature to
coordinate the same concept.

We might also allow richer objects to be passed along here. Only the
strings would apply to View Transitions but the imperative code and
effects could do something else with them.
2025-01-21 15:00:02 -05:00
Sebastian Markbåge
0bf1f39ec6
View Transition Refs (#32038)
This adds refs to View Transition that can resolve to an instance of:

```js
type ViewTransitionRef = {
  name: string,
  group: Animatable,
  imagePair: Animatable,
  old: Animatable,
  new: Animatable,
}
```

Animatable is a type that has `animate(keyframes, options)` and
`getAnimations()` on it. It's the interface that exists on Element that
lets you start animations on it. These ones are like that but for the
four pseudo-elements created by the view transition.

If a name changes, then a new ref is created. That way if you hold onto
a ref during an exit animation spawned by the name change, you can keep
calling functions on it. It will keep referring to the old name rather
than the new name.

This allows imperative control over the animations instead of using CSS
for this.

```js
const viewTransition = ref.current;
const groupAnimation = viewTransition.group.animate(keyframes, options);
const imagePairAnimation = viewTransition.imagePair.animate(keyframes, options);
const oldAnimation = viewTransition.old.animate(keyframes, options);
const newAnimation = viewTransition.new.animate(keyframes, options);
```

The downside of using this API is that it doesn't work with SSR so for
SSR rendered animations they'll fallback to the CSS. You could use this
for progressive enhancement though.

Note: In this PR the ref only controls one DOM node child but there can
be more than one DOM node in the ViewTransition fragment and they are
just left to their defaults. We could try something like making the
`animate()` function apply to multiple children but that could lead to
some weird consequences and the return value would be difficult to
merge. We could try to maintain an array of Animatable that updates with
how ever many things are currently animating but that makes the API more
complicated to use for the simple case. Conceptually this should be like
a fragment so we would ideally combine the multiple children into a
single isolate if we could. Maybe one day the same name could be applied
to multiple children to create a single isolate. For now I think I'll
just leave it like this and you're really expect to just use it with one
DOM node. If you have more than one they just get the default animations
from CSS.

Using this is a little tricky due timing. In this fixture I just use a
layout effect plus rAF to get into the right timing after the
startViewTransition is ready. In the future I'll add an event that fires
when View Transitions heuristics fire with the right timing.
2025-01-10 11:51:37 -05:00
Sebastian Markbåge
fd9cfa416f
Execute layout phase before after mutation phase inside view transition (#32029)
This allows mutations and scrolling in the layout phase to be counted
towards the mutation. This would maybe not be the case for gestures but
it is useful for fire-and-forget.

This also avoids the issue that if you resolve navigation in
useLayoutEffect that it ends up dead locked.

It also means that useLayoutEffect does not observe the scroll
restoration and in fact, the scroll restoration would win over any
manual scrolling in layout effects. For better or worse, this is more in
line with how things worked before and how it works in popstate. So it's
less of a breaking change. This does mean that we can't unify the after
mutation phase with the layout phase though.

To do this we need split out flushSpawnedWork from the flushLayoutEffect
call.

Spawned work from setState inside the layout phase is done outside and
not counted towards the transition. They're sync updates and so are not
eligible for their own View Transitions. It's also tricky to support
this since it's unclear what things like exits in that update would
mean. This work will still be able to mutate the live DOM but it's just
not eligible to trigger new transitions or adjust the target of those.

One difference between popstate is that this spawned work is after
scroll restoration. So any scrolling spawned from a second pass would
now win over scroll restoration.

Another consequence of this change is that you can't safely animate
pseudo elements in useLayoutEffect. We'll introduce a better event for
that anyway.
2025-01-08 19:13:25 -05:00
Sebastian Markbåge
98418e8902
[Fiber] Suspend the commit while we wait for the previous View Transition to finish (#32002)
Stacked on #31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.
2025-01-08 13:36:57 -05:00
Sebastian Markbåge
3a5496b3f5
[Fiber] Use className on <ViewTransition> to assign view-transition-class (#31999)
Stacked on #31975.

This is the primary way we recommend styling your View Transitions since
it allows for reusable styling such as a CSS library specializing in
View Transitions in a way that's composable and without naming
conflicts. E.g.

```js
<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">
```

This doesn't change the HTML `class` attribute. It's not a CSS class.
Instead it assign the `view-transition-class` style prop of the
underlying DOM node while it's transitioning.

You can also just use `<div style={{viewTransitionClass: ...}}>` on the
DOM node but it's convenient to control the Transition completely from
the outside and conceptually we're transitioning the whole fragment. You
can even make Transition components that just wraps existing components.
`<RevealTransition><Component /></RevealTransition>` this way.

Since you can also have multiple wrappers for different circumstances it
allows React's heuristics to use different classes for different
scenarios. We'll likely add more options like configuring different
classes for different `types` or scenarios that can't be described by
CSS alone.

## CSS Modules

```js
import transitions from './transitions.module.css';

<ViewTransition className={transitions.bounceIn}>...</ViewTransition>
```

CSS Modules works well with this strategy because you can have globally
unique namespaces and define your transitions in the CSS modules as a
library that you can import. [As seen in the fixture
here.](8b91b37bb8 (diff-b4d9854171ffdac4d2c01be92a5eff4f8e9e761e6af953094f99ca243b054a85R11))

I did notice an unfortunate bug in how CSS Modules (at least in Webpack)
generates class names. Sometimes the `+` character is used in the hash
of the class name which is not valid for `view-transition-class` and so
it breaks. I had to rename my class names until the hash yielded
something different to work around it. Ideally that bug gets fixed soon.

## className, rly?

`className` isn't exactly the most loved property name, however, I'm
using `className` here too for consistency. Even though in this case
there's no direct equivalent DOM property name. The CSS property is
named `viewTransitionClass`, but the "viewTransition" prefix is implied
by the Component it is on in this case. For most people the fact that
this is actually a different namespace than other CSS classes doesn't
matter. You'll most just use a CSS library anyway and conceptually
you're just assigning classes the same way as `className` on a DOM node.

But if we ever rename the `class` prop then we can do that for this one
as well.
2025-01-08 13:22:06 -05:00
Sebastian Markbåge
a4d122f2d1
Add <ViewTransition> Component (#31975)
This will provide the opt-in for using [View
Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
in React.

View Transitions only trigger for async updates like `startTransition`,
`useDeferredValue`, Actions or `<Suspense>` revealing from fallback to
content. Synchronous updates provide an opt-out but also guarantee that
they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like
event handlers or actions. They don't know what UI will change and
whether that has an animated transition described.

Conceptually the `<ViewTransition>` component is like a DOM fragment
that transitions its children in its own isolate/snapshot. The API works
by wrapping a DOM node or inner component:

```js
import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>
```

The default is `name="auto"` which will automatically assign a
`view-transition-name` to the inner DOM node. That way you can add a
View Transition to a Component without controlling its DOM nodes styling
otherwise.

A difference between this and the browser's built-in
`view-transition-name: auto` is that switching the DOM nodes within the
`<ViewTransition>` component preserves the same name so this example
cross-fades between the DOM nodes instead of causing an exit and enter:

```js
<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>
```

This becomes especially useful with `<Suspense>` as this example
cross-fades between Skeleton and Content:

```js
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
```

Where as this example triggers an exit of the Skeleton and an enter of
the Content:

```js
<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>
```

Managing instances and keys becomes extra important.

You can also specify an explicit `name` property for example for
animating the same conceptual item from one page onto another. However,
best practices is to property namespace these since they can easily
collide. It's also useful to add an `id` to it if available.

```js
<ViewTransition name="my-shared-view">
```

The model in general is the same as plain `view-transition-name` except
React manages a set of heuristics for when to apply it. A problem with
the naive View Transitions model is that it overly opts in every
boundary that *might* transition into transitioning. This is leads to
unfortunate effects like things floating around when unrelated updates
happen. This leads the whole document to animate which means that
nothing is clickable in the meantime. It makes it not useful for smaller
and more local transitions. Best practice is to add
`view-transition-name` only right before you're about to need to animate
the thing. This is tricky to manage globally on complex apps and is not
compositional. Instead we let React manage when a `<ViewTransition>`
"activates" and add/remove the `view-transition-name`. This is also when
React calls `startViewTransition` behind the scenes while it mutates the
DOM.

I've come up with a number of heuristics that I think will make a lot
easier to coordinate this. The principle is that only if something that
updates that particular boundary do we activate it. I hope that one day
maybe browsers will have something like these built-in and we can remove
our implementation.

A `<ViewTransition>` only activates if:

- If a mounted Component renders a `<ViewTransition>` within it outside
the first DOM node, and it is within the viewport, then that
ViewTransition activates as an "enter" animation. This avoids inner
"enter" animations trigger when the parent mounts.
- If an unmounted Component had a `<ViewTransition>` within it outside
the first DOM node, and it was within the viewport, then that
ViewTransition activates as an "exit" animation. This avoids inner
"exit" animations triggering when the parent unmounts.
- If an explicitly named `<ViewTransition name="...">` is deep within an
unmounted tree and one with the same name appears in a mounted tree at
the same time, then both are activated as a pair, but only if they're
both in the viewport. This avoids these triggering "enter" or "exit"
animations when going between parents that don't have a pair.
- If an already mounted `<ViewTransition>` is visible and a DOM
mutation, that might affect how it's painted, happens within its
children but outside any nested `<ViewTransition>`. This allows it to
"cross-fade" between its updates.
- If an already mounted `<ViewTransition>` resizes or moves as the
result of direct DOM nodes siblings changing or moving around. This
allows insertion, deletion and reorders into a list to animate all
children. It is only within one DOM node though, to avoid unrelated
changes in the parent to trigger this. If an item is outside the
viewport before and after, then it's skipped to avoid things flying
across the screen.
- If a `<ViewTransition>` boundary changes size, due to a DOM mutation
within it, then the parent activates (or the root document if there are
no more parents). This ensures that the container can cross-fade to
avoid abrupt relayout. This can be avoided by using absolutely
positioned children. When this can avoid bubbling to the root document,
whatever is not animating is still responsive to clicks during the
transition.

Conceptually each DOM node has its own default that activates the parent
`<ViewTransition>` or no transition if the parent is the root. That
means that if you add a DOM node like `<div><ViewTransition><Component
/></ViewTransition></div>` this won't trigger an "enter" animation since
it was the div that was added, not the ViewTransition. Instead, it might
cause a cross-fade of the parent ViewTransition or no transition if it
had no parent. This ensures that only explicit boundaries perform coarse
animations instead of every single node which is really the benefit of
the View Transitions model. This ends up working out well for simple
cases like switching between two pages immediately while transitioning
one floating item that appears on both pages. Because only the floating
item transitions by default.

Note that it's possible to add manual `view-transition-name` with CSS or
`style={{ viewTransitionName: 'auto' }}` that always transitions as long
as something else has a `<ViewTransition>` that activates. For example a
`<ViewTransition>` can wrap a whole page for a cross-fade but inside of
it an explicit name can be added to something to ensure it animates as a
move when something relates else changes its layout. Instead of just
cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded
APIs. This first PR explores the above core heuristic.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-01-08 12:11:18 -05:00
Ricky
26297f5383
[assert helpers] not dom or reconciler (#31862)
converts everything left outside react-dom and react-reconciler
2024-12-20 12:41:13 -05:00
Andrew Clark
c86542b240
Bump next prerelease version numbers (#31676)
Updates the version numbers in the prerelease (canary and experimental)
channels.

---------

Co-authored-by: Jack Pope <jackpope1@gmail.com>
2024-12-12 14:10:46 -05:00
Sebastian Markbåge
c13986da78
Fix Overlapping "message" Bug in Performance Track (#31528)
When you schedule a microtask from render or effect and then call
setState (or ping) from there, the "event" is the event that React
scheduled (which will be a postMessage). The event time of this new
render will be before the last render finished.

We usually clamp these but in this scenario the update doesn't happen
while a render is happening. Causing overlapping events.

Before:

<img width="1229" alt="Screenshot 2024-11-12 at 11 01 30 PM"
src="https://github.com/user-attachments/assets/9652cf3b-b358-453c-b295-1239cbb15952">

Therefore when we finalize a render we need to store the end of the last
render so when we a new update comes in later with an event time earlier
than that, we know to clamp it.

There's also a special case here where when we enter the
`RootDidNotComplete` or `RootSuspendedWithDelay` case we neither leave
the root as in progress nor commit it. Those needs to finalize too.
Really this should be modeled as a suspended track that we haven't added
yet. That's the gap between "Blocked" and "message" below.

After:

<img width="1471" alt="Screenshot 2024-11-13 at 12 31 34 AM"
src="https://github.com/user-attachments/assets/b24f994e-9055-4b10-ad29-ad9b36302ffc">

I also fixed an issue where we may log the same event name multiple
times if we're rendering more than once in the same event. In this case
I just leave a blank trace between the last commit and the next update.

I also adding ignoring of the "message" event at all in these cases when
the event is from React's scheduling itself.
2024-11-14 16:35:08 -05:00
Jan Kassens
07aa494432
Remove enableRefAsProp feature flag (#30346)
The flag is fully rolled out.
2024-11-04 14:30:58 -05:00
Sebastian Markbåge
d4688dfaaf
[Fiber] Track Event Time, startTransition Time and setState Time (#31008)
This tracks the current window.event.timeStamp the first time we
setState or call startTransition. For either the blocking track or
transition track. We can use this to show how long we were blocked by
other events or overhead from when the user interacted until we got
called into React.

Then we track the time we start awaiting a Promise returned from
startTransition. We can use this track how long we waited on an Action
to complete before setState was called.

Then finally we track when setState was called so we can track how long
we were blocked by other word before we could actually start rendering.
For a Transition this might be blocked by Blocking React render work.

We only log these once a subsequent render actually happened. If no
render was actually scheduled, then we don't log these. E.g. if an
isomorphic Action doesn't call startTransition there's no render so we
don't log it.

We only log the first event/update/transition even if multiple are
batched into it later. If multiple Actions are entangled they're all
treated as one until an update happens. If no update happens and all
entangled actions finish, we clear the transition so that the next time
a new sequence starts we can log it.

We also clamp these (start the track later) if they were scheduled
within a render/commit. Since we share a single track we don't want to
create overlapping tracks.

The purpose of this is not to show every event/action that happens but
to show a prelude to how long we were blocked before a render started.
So you can follow the first event to commit.

<img width="674" alt="Screenshot 2024-09-20 at 1 59 58 AM"
src="https://github.com/user-attachments/assets/151ba9e8-6b3c-4fa1-9f8d-e3602745eeb7">

I still need to add the rendering/suspended phases to the timeline which
why this screenshot has a gap.

<img width="993" alt="Screenshot 2024-09-20 at 12 50 27 AM"
src="https://github.com/user-attachments/assets/155b6675-b78a-4a22-a32b-212c15051074">

In this case it's a Form Action which started a render into the form
which then suspended on the action. The action then caused a refresh,
which interrupts with its own update that's blocked before rendering.
Suspended roots like this is interesting because we could in theory
start working on a different root in the meantime which makes this
timeline less linear.
2024-09-20 14:27:12 -04:00
Sam Zhou
e210d08180
[flow] Upgrade Flow to 0.245.2 (#30919)
## Summary

This PR bumps Flow all the way to the latest 0.245.2. 

Most of the suppressions comes from Flow v0.239.0's change to include
undefined in the return of `Array.pop`.

I also enabled `react.custom_jsx_typing=true` and added custom jsx
typing to match the old behavior that `React.createElement` is
effectively any typed. This is necessary since various builtin
components like `React.Fragment` is actually symbol in the React repo
instead of `React.AbstractComponent<...>`. It can be made more accurate
by customizing the `React$CustomJSXFactory` type, but I will leave it to
the React team to decide.

## How did you test this change?

`yarn flow` for all the renderers
2024-09-09 08:41:44 -07:00
Sebastian Silbermann
88ee14ffa5
[Devtools] Ensure initial read of useFormStatus returns NotPendingTransition (#28728) 2024-08-01 10:55:53 +02:00
Sebastian Markbåge
146df7c311
[Fiber] Make DevTools Config use Static Injection (#30522)
We use static dependency injection. We shouldn't use this dynamic
dependency injection we do for DevTools internals. There's also meta
programming like spreading and stuff that isn't needed.

This moves the config from `injectIntoDevTools` to the FiberConfig so it
can be statically resolved.

Closure Compiler has some trouble generating optimal code for this
anyway so ideally we'd refactor this further but at least this is better
and saves a few bytes and avoids some code paths (when minified).
2024-07-30 15:03:54 -04:00
Jack Pope
14a4699ff1
Remove allowConcurrentByDefault flag (#30445)
Following https://github.com/facebook/react/pull/30436

Concurrent by default strategy has been unshipped. Here we clean up the
`allowConcurrentByDefault` path and related logic/tests.

For now, this keeps the `concurrentUpdatesByDefaultOverride` argument in
`createContainer` and `createHydrationContainer` and ignores the value
to prevent more breaking changes to `react-reconciler` in the RC stage.
2024-07-25 11:59:50 -04:00
Jan Kassens
b565373afd
lint: enable reportUnusedDisableDirectives and remove unused suppressions (#28721)
This enables linting against unused suppressions and removes the ones
that were unused.
2024-06-21 12:24:32 -04:00
Sebastian Markbåge
6b4646cbd0
Export captureOwnerStacks() only in DEV "react" builds (#29923)
Only with the enableOwnerStacks flag (which is not on in www).

This is a new DEV-only API to be able to implement what we do for
console.error in user space.

This API does not actually include the current stack that you'd get from
`new Error().stack`. That you'd have to add yourself.

This adds the ability to have conditional development exports because we
plan on eventually having separate ESM builds that use the "development"
or "production" export conditions.

NOTE: This removes the export of `act` from `react` in prod (as opposed
to a function that throws) - inline with what we do with other
conditional exports.
2024-06-19 14:19:48 -04:00
Sebastian Markbåge
2774208039
Remove Warning: prefix and toString on console Arguments (#29839)
Basically make `console.error` and `console.warn` behave like normal -
when a component stack isn't appended. I need this because I need to be
able to print rich logs with the component stack option and to be able
to disable instrumentation completely in `console.createTask`
environments that don't need it.

Currently we can't print logs with richer objects because they're
toString:ed first. In practice, pretty much all arguments we log are
already toString:ed so it's not necessary anyway. Some might be like a
number. So it would only be a problem if some environment can't handle
proper consoles but then it's up to that environment to toString it
before logging.

The `Warning: ` prefix is historic and is both noisy and confusing. It's
mostly unnecessary since the UI surrounding `console.error` and
`console.warn` tend to have visual treatment around it anyway. However,
it's actively misleading when `console.error` gets prefixed with a
Warning that we consider an error level. There's an argument to be made
that some of our `console.error` don't make the bar for an error but
then the argument is to downgrade each of those to `console.warn` - not
to brand all our actual error logging with `Warning: `.

Apparently something needs to change in React Native before landing this
because it depends on the prefix somehow which probably doesn't make
sense already.
2024-06-10 18:41:56 -04:00
Ricky
d172bdaf95
Add jest lint rules (#29760)
## Overview

Updates `eslint-plugin-jest` and enables the recommended rules with some
turned off that are unhelpful.

The main motivations is:
a) we have a few duplicated tests, which this found an I deleted 
b) making sure we don't accidentally commit skipped tests
2024-06-10 14:31:37 -04:00
Sebastian Silbermann
82d8129e58
Reconciler: Change commitUpdate signature to account for unused updatePayload parameter (#28909) 2024-04-25 19:14:06 +02:00
Andrew Clark
c516cefc7d
warn -> error for Test Renderer deprecation (#28904)
We use `console.error` for deprecations. `console.warn` is for less
critical issues, like performance anti-patterns.
2024-04-24 14:54:39 -04:00
Andrew Clark
857ee8cdf9
Don't minify symbols in production builds (#28881)
This disables symbol renaming in production builds. The original
variable and function names are preserved. All other forms of
compression applied by Closure (dead code elimination, inlining, etc)
are unchanged — the final program is identical to what we were producing
before, just in a more readable form.

The motivation is to make it easier to debug React issues that only
occur in production — the same reason we decided to start shipping
sourcemaps in #28827 and #28827.

However, because most apps run their own minification step on their npm
dependencies, it's not necessary for us to minify the symbols before
publishing — it'll be handled the app, if desired.

This is the same strategy Meta has used to ship React for years. The
React build itself has unminified symbols, but they get minified as part
of Meta's regular build pipeline.

Even if an app does not minify their npm dependencies, gzip covers most
of the cost of symbol renaming anyway.

This saves us from having to ship sourcemaps, which means even apps that
don't have sourcemaps configured will be able to debug the React build
as easily as they would any other npm dependency.
2024-04-20 11:23:46 -04:00
Josh Story
da6ba53b10
[UMD] Remove umd builds (#28735)
In React 19 React will finally stop publishing UMD builds. This is
motivated primarily by the lack of use of UMD format and the added
complexity of maintaining build infra for these releases. Additionally
with ESM becoming more prevalent in browsers and services like esm.sh
which can host React as an ESM module there are other options for doing
script tag based react loading.

This PR removes all the UMD build configs and forks.

There are some fixtures that still have references to UMD builds however
many of them already do not work (for instance they are using legacy
features like ReactDOM.render) and rather than block the removal on
these fixtures being brought up to date we'll just move forward and fix
or removes fixtures as necessary in the future.
2024-04-17 11:15:27 -07:00
Andrew Clark
41950d14a5
Automatically reset forms after action finishes (#28804)
This updates the behavior of form actions to automatically reset the
form's uncontrolled inputs after the action finishes.

This is a frequent feature request for people using actions and it
aligns the behavior of client-side form submissions more closely with
MPA form submissions.

It has no impact on controlled form inputs. It's the same as if you
called `form.reset()` manually, except React handles the timing of when
the reset happens, which is tricky/impossible to get exactly right in
userspace.

The reset shouldn't happen until the UI has updated with the result of
the action. So, resetting inside the action is too early.

Resetting in `useEffect` is better, but it's later than ideal because
any effects that run before it will observe the state of the form before
it's been reset.

It needs to happen in the mutation phase of the transition. More
specifically, after all the DOM mutations caused by the transition have
been applied. That way the `defaultValue` of the inputs are updated
before the values are reset. The idea is that the `defaultValue`
represents the current, canonical value sent by the server.

Note: this change has no effect on form submissions that aren't
triggered by an action.
2024-04-10 16:54:24 -04:00