mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
component-tree-tool
31 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
3c3696d554 |
Measure Updated ViewTransition Boundaries (#32653)
This does the same thing for `measureUpdateViewTransition` that we did for `measureNestedViewTransitions` in https://github.com/facebook/react/pull/32612/commits/e3cbaffef05c7b476c07f7495e06788a9503e636. 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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.](https://github.com/facebook/react/commit/8b91b37bb8b4add5f3f8be5ef8f49bb23966b13b#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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
88ee14ffa5 |
[Devtools] Ensure initial read of useFormStatus returns NotPendingTransition (#28728)
|
||
|
|
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). |
||
|
|
82d8129e58 |
Reconciler: Change commitUpdate signature to account for unused updatePayload parameter (#28909)
|
||
|
|
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. |
||
|
|
8e1462e8c4 |
[Fiber] Move updatePriority tracking to renderers (#28751)
Currently updatePriority is tracked in the reconciler. `flushSync` is going to be implemented reconciler agnostic soon and we need to move the tracking of this state to the renderer and out of reconciler. This change implements new renderer bin dings for getCurrentUpdatePriority and setCurrentUpdatePriority. I was originally going to have the getter also do the event priority defaulting using window.event so we eliminate getCur rentEventPriority but this makes all the callsites where we store the true current updatePriority on the stack harder to work with so for now they remain separate. I also moved runWithPriority to the renderer since it really belongs whereever the state is being managed and it is only currently exposed in the DOM renderer. Additionally the current update priority is not stored on ReactDOMSharedInternals. While not particularly meaningful in this change it opens the door to implementing `flushSync` outside of the reconciler |
||
|
|
7f6201889e |
Ship diffInCommitPhase (#27409)
Performance tests at Meta showed neutral results. |
||
|
|
fda1f0b902 |
Flow upgrade to 0.205.1 (#26796)
Just a small upgrade to keep us current and remove unused suppressions (probably fixed by some upgrade since). - `*` is no longer allowed and has been an alias for `any` for a while now. |
||
|
|
540bab085d |
Implement experimental_useFormStatus (#26722)
This hook reads the status of its ancestor form component, if it exists.
```js
const {pending, data, action, method} = useFormStatus();
```
It can be used to implement a loading indicator, for example. You can
think of it as a shortcut for implementing a loading state with the
useTransition hook.
For now, it's only available in the experimental channel. We'll share
docs once its closer to being stable. There are additional APIs that
will ship alongside it.
Internally it's implemented using startTransition + a context object.
That's a good way to think about its behavior, but the actual
implementation details may change in the future.
Because form elements cannot be nested, the implementation in the
reconciler does not bother to keep track of multiple nested "transition
providers". So although it's implemented using generic Fiber config
methods, it does currently make some assumptions based on React DOM's
requirements.
|
||
|
|
fdad813ac7 |
[Float][Fiber] Enable Float methods to be called outside of render (#26557)
Stacked on #26570 Previously we restricted Float methods to only being callable while rendering. This allowed us to make associations between calls and their position in the DOM tree, for instance hoisting preinitialized styles into a ShadowRoot or an iframe Document. When considering how we are going to support Flight support in Float however it became clear that this restriction would lead to compromises on the implementation because the Flight client does not execute within the context of a client render. We want to be able to disaptch Float directives coming from Flight as soon as possible and this requires being able to call them outside of render. this patch modifies Float so that its methods are callable anywhere. The main consequence of this change is Float will always use the Document the renderer script is running within as the HoistableRoot. This means if you preinit as style inside a component render targeting a ShadowRoot the style will load in the ownerDocument not the ShadowRoot. Practially speaking it means that preinit is not useful inside ShadowRoots and iframes. This tradeoff was deemed acceptable because these methods are optimistic, not critical. Additionally, the other methods, preconntect, prefetchDNS, and preload, are not impacted because they already operated at the level of the ownerDocument and really only interface with the Network cache layer. I added a couple additional fixes that were necessary for getting tests to pass that are worth considering separately. The first commit improves the diff for `waitForThrow` so it compares strings if possible. The second commit makes invokeGuardedCallback not use metaprogramming pattern and swallows any novel errors produced from trying to run the guarded callback. Swallowing may not be the best we can do but it at least protects React against rapid failure when something causes the dispatchEvent to throw. |
||
|
|
d121c67004 |
Synchronously flush the transition lane scheduled in a popstate event (#26025)
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary Browsers restore state like forms and scroll position right after the popstate event. To make sure the page work as expected on back or forward button, we need to flush transitions scheduled in a popstate synchronously, and only yields if it suspends. This PR adds a new HostConfig method to check if `window.event === 'popstate'`, and `scheduleMicrotask` if a transition is scheduled in a `PopStateEvent`. ## How did you test this change? yarn test |
||
|
|
ca41adb8c1 |
Diff properties in the commit phase instead of generating an update payload (#26583)
This removes the concept of `prepareUpdate()`, behind a flag. React Native already does everything in the commit phase, but generates a temporary update payload before applying it. React Fabric does it both in the render phase. Now it just moves it to a single host config. For DOM I forked updateProperties into one that does diffing and updating in one pass vs just applying a pre-diffed updatePayload. There are a few downsides of this approach: - If only "children" has changed, we end up scheduling an update to be done in the commit phase. Since we traverse through it anyway, it's probably not much extra. - It does more work in the commit phase so for a large tree that is mostly unchanged, it'll stall longer. - It does some extra work for special cases since that work happens if anything has changed. We no longer have a deep bailout. - The special cases now have to each replicate the "clean up old props" loop, leading to extra code. The benefit is that this doesn't allocate temporary extra objects (possibly multiple per element if the array has to resize). It's less work overall. It also gives us an option to reuse this function for a sync render optimization. Another benefit is that if we do the loop in the commit phase I can do further optimizations by reading all props that I need for special cases in that loop instead of polymorphic reads from props. This is what I'd like to do in future refactors that would be stacked on top of this change. |
||
|
|
b55d319559 |
Rename HostConfig files to FiberConfig to clarify they are configs fo… (#26592)
part of https://github.com/facebook/react/pull/26571 merging separately to improve tracking of files renames in git Rename HostConfig files to FiberConfig to clarify they are configs for Fiber and not Fizz/Flight. This better conforms to the naming used in Flight and now Fizz of `ReactFlightServerConfig` and `ReactFizzConfig` |