-
Notifications
You must be signed in to change notification settings - Fork 47.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add <ViewTransition> Component #31975
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
d1ff372
to
d8c7f5a
Compare
Wondering how this will work in RN |
How can I set the transition to duration? |
The idea is that this can be implemented on the native side with a form of Shared Element Transitions. It would however likely be a new implementation (as opposed to mapping to Layout Animations or Reanimated) to ensure it lines up semantically with the web's View Transitions model so it's not slightly different on native. I think this would likely only be available on the New Architecture. The way you style these are a bit up on the air since on web it heavily relies on CSS, but the general gist is that a View Transition class could be registered with Native and you'd specify a name class on the JS side. There will be a programmatic API that can work on RN and web. Similarly, libraries like react-three-fiber could implement GL based transitions as long as it follows the general model and implements the required config hooks that React exposes. The idea is that this would be a general purpose View Transition / Shared Element Animation model across platforms. That's why this Component is exposed on |
d8c7f5a
to
d0e69fe
Compare
…but flush it eagerly if we're sync (#31987) This is a follow up to #31930 and a prerequisite for #31975. With View Transitions, the commit phase becomes async which means that other work can sneak in between. We need to be resilient to that. This PR first refactors the flushMutationEffects and flushLayoutEffects to use module scope variables to track its arguments so we can defer them. It shares these with how we were already doing it for flushPendingEffects. We also track how far along the commit phase we are so we know what we have left to flush. Then callers of flushPassiveEffects become flushPendingEffects. That helper synchronously flushes any remaining phases we've yet to commit. That ensure that things are at least consistent if that happens. Finally, when we are using a scheduled task, we don't do any work. This ensures that we're not flushing any work too early if we could've deferred it. This still ensures that we always do flush it before starting any new work on any root so new roots observe the committed state. There are some unfortunate effects that could happen from allowing things to flush eagerly. Such as if a flushSync sneaks in before startViewTransition, it'll skip the animation. If it's during a suspensey font it'll start the transition before the font has loaded which might be better than breaking flushSync. It'll also potentially flush passive effects inside the startViewTransition which should typically be ok.
We currently use the useId algorithm for this for hydration in case we want to support animating from server rendered fallback to client rendered content. It might not be needed though since we only apply the names temporarily.
This will let us know if we have any View Transitions in this subtree that may run an enter/exit animation. It provides a fast path for finding them during a placement or deletion.
Only if this is an eligible lane - Transition, Retry or Idle.
Do the same thing as the snapshot phase in reverse. We could use either the layout phase or passive phase for this. We use the passive in case this creates any extra work but it might actually be the reverse if this is work that would've been done anyway before the first paint. So we could also try this in the layout phase.
…e after mutation phase we can assign the name there This lets us avoid an extra traversal in the mutation phase.
… name This lets us find it deep within a deleted or reappearing tree do that we can connect it with its pair.
…eappearing We need to know this before we visit the deletions in the snapshot phase so we know if there is a pair.
…with the same names
That way we'll be able to exclude them if they are not within the viewport. To do this we need a different mechanisms to detect insertions instead of Placement effect which was already wrong since it includes moves.
…viewport These shouldn't be visible anyway.
This avoids things flying in, onto, or across the screen unexpectedly. Unfortunately if it goes from onscreen to offscreen we have no way to cancel the old state, so that appears as an exit even though it should've just painted on top of its deleted parent if we had known.
d0e69fe
to
888ccc6
Compare
…but flush it eagerly if we're sync (#31987) This is a follow up to #31930 and a prerequisite for #31975. With View Transitions, the commit phase becomes async which means that other work can sneak in between. We need to be resilient to that. This PR first refactors the flushMutationEffects and flushLayoutEffects to use module scope variables to track its arguments so we can defer them. It shares these with how we were already doing it for flushPendingEffects. We also track how far along the commit phase we are so we know what we have left to flush. Then callers of flushPassiveEffects become flushPendingEffects. That helper synchronously flushes any remaining phases we've yet to commit. That ensure that things are at least consistent if that happens. Finally, when we are using a scheduled task, we don't do any work. This ensures that we're not flushing any work too early if we could've deferred it. This still ensures that we always do flush it before starting any new work on any root so new roots observe the committed state. There are some unfortunate effects that could happen from allowing things to flush eagerly. Such as if a flushSync sneaks in before startViewTransition, it'll skip the animation. If it's during a suspensey font it'll start the transition before the font has loaded which might be better than breaking flushSync. It'll also potentially flush passive effects inside the startViewTransition which should typically be ok. DiffTrain build for [defffdb](defffdb)
…but flush it eagerly if we're sync (#31987) This is a follow up to #31930 and a prerequisite for #31975. With View Transitions, the commit phase becomes async which means that other work can sneak in between. We need to be resilient to that. This PR first refactors the flushMutationEffects and flushLayoutEffects to use module scope variables to track its arguments so we can defer them. It shares these with how we were already doing it for flushPendingEffects. We also track how far along the commit phase we are so we know what we have left to flush. Then callers of flushPassiveEffects become flushPendingEffects. That helper synchronously flushes any remaining phases we've yet to commit. That ensure that things are at least consistent if that happens. Finally, when we are using a scheduled task, we don't do any work. This ensures that we're not flushing any work too early if we could've deferred it. This still ensures that we always do flush it before starting any new work on any root so new roots observe the committed state. There are some unfortunate effects that could happen from allowing things to flush eagerly. Such as if a flushSync sneaks in before startViewTransition, it'll skip the animation. If it's during a suspensey font it'll start the transition before the font has loaded which might be better than breaking flushSync. It'll also potentially flush passive effects inside the startViewTransition which should typically be ok. DiffTrain build for [defffdb](defffdb)
…but flush it eagerly if we're sync (facebook#31987) This is a follow up to facebook#31930 and a prerequisite for facebook#31975. With View Transitions, the commit phase becomes async which means that other work can sneak in between. We need to be resilient to that. This PR first refactors the flushMutationEffects and flushLayoutEffects to use module scope variables to track its arguments so we can defer them. It shares these with how we were already doing it for flushPendingEffects. We also track how far along the commit phase we are so we know what we have left to flush. Then callers of flushPassiveEffects become flushPendingEffects. That helper synchronously flushes any remaining phases we've yet to commit. That ensure that things are at least consistent if that happens. Finally, when we are using a scheduled task, we don't do any work. This ensures that we're not flushing any work too early if we could've deferred it. This still ensures that we always do flush it before starting any new work on any root so new roots observe the committed state. There are some unfortunate effects that could happen from allowing things to flush eagerly. Such as if a flushSync sneaks in before startViewTransition, it'll skip the animation. If it's during a suspensey font it'll start the transition before the font has loaded which might be better than breaking flushSync. It'll also potentially flush passive effects inside the startViewTransition which should typically be ok. DiffTrain build for [defffdb](facebook@defffdb)
…but flush it eagerly if we're sync (facebook#31987) This is a follow up to facebook#31930 and a prerequisite for facebook#31975. With View Transitions, the commit phase becomes async which means that other work can sneak in between. We need to be resilient to that. This PR first refactors the flushMutationEffects and flushLayoutEffects to use module scope variables to track its arguments so we can defer them. It shares these with how we were already doing it for flushPendingEffects. We also track how far along the commit phase we are so we know what we have left to flush. Then callers of flushPassiveEffects become flushPendingEffects. That helper synchronously flushes any remaining phases we've yet to commit. That ensure that things are at least consistent if that happens. Finally, when we are using a scheduled task, we don't do any work. This ensures that we're not flushing any work too early if we could've deferred it. This still ensures that we always do flush it before starting any new work on any root so new roots observe the committed state. There are some unfortunate effects that could happen from allowing things to flush eagerly. Such as if a flushSync sneaks in before startViewTransition, it'll skip the animation. If it's during a suspensey font it'll start the transition before the font has loaded which might be better than breaking flushSync. It'll also potentially flush passive effects inside the startViewTransition which should typically be ok. DiffTrain build for [defffdb](facebook@defffdb)
Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
c59eb46
to
b2d126f
Compare
We were reading from the wrong fiber. Update the fixture to show that the document and the button remains interactive due to the inner boundary not resizing.
This will provide the opt-in for using View Transitions 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:The default is
name="auto"
which will automatically assign aview-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:This becomes especially useful with
<Suspense>
as this example cross-fades between Skeleton and Content:Where as this example triggers an exit of the Skeleton and an enter of the Content:
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 anid
to it if available.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 addview-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 theview-transition-name
. This is also when React callsstartViewTransition
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:<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.<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.<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.<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.<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.<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 orstyle={{ 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.