Skip to content

ViewTransitions in Navigation#32028

Merged
sebmarkbage merged 5 commits into
facebook:mainfrom
sebmarkbage:viewtransitionnav
Jan 8, 2025
Merged

ViewTransitions in Navigation#32028
sebmarkbage merged 5 commits into
facebook:mainfrom
sebmarkbage:viewtransitionnav

Conversation

@sebmarkbage
Copy link
Copy Markdown
Contributor

This adds navigation support to the View Transition fixture using both history.pushState/popstate and the Navigation API models.

Because popstate does scroll restoration synchronously at the end of the event, but startViewTransition cannot start synchronously, it would observe the "old" state as after applying scroll restoration. This leads to weird artifacts. So we intentionally do not support View Transitions in popstate. If it suspends anyway for some other reason, then scroll restoration is broken anyway and then it is supported. We don't have to do anything here because this is already how things worked because the sync popstate special case already included the sync lane which opts it out of View Transitions.

For the Navigation API, scroll restoration can be blocked. The best way to do this is to resolve the Navigation API promise after React has applied its mutation. We can detect if there's currently any pending navigation and wait to resolve the startViewTransition until it finishes and any scroll restoration has been applied.

scroll-restoration.mov

There is a subtle thing here. If we read the viewport metrics before scroll restoration has been applied, then we might assume something is or isn't going to be within the viewport incorrectly. This is evident on the "Slide In from Left" example. When we're going forward to that page we shift the scroll position such that it's going to appear in the viewport. If we did this before applying scroll restoration, it would not animate because it wasn't in the viewport then. Therefore, we need to run the after mutation phase after scroll restoration.

A consequence of this is that you have to resolve Navigation in useInsertionEffect as otherwise it leads to a deadlock (which eventually gets broken by startViewTransition's timeout of 10 seconds). Another consequence is that now useLayoutEffect observes the restored state. However, I think what we'll likely do is move the layout phase to before the after mutation phase which also ensures that auto-scrolling inside useLayoutEffect are considered in the viewport measurements as well.

@react-sizebot
Copy link
Copy Markdown

react-sizebot commented Jan 8, 2025

Comparing: 98418e8902d6045e5138a2e765e026ce2e4de82d...70080fb34a0a135605859cf824255d09c7bc4216

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 513.92 kB 513.86 kB = 91.78 kB 91.78 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.05% 546.68 kB 546.98 kB +0.06% 97.25 kB 97.31 kB
facebook-www/ReactDOM-prod.classic.js = 595.82 kB 595.76 kB = 104.87 kB 104.86 kB
facebook-www/ReactDOM-prod.modern.js = 586.25 kB 586.19 kB = 103.32 kB 103.31 kB
oss-experimental/react-art/cjs/react-art.production.js = 324.85 kB 318.06 kB = 54.96 kB 54.13 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
test_utils/ReactAllWarnings.js +0.38% 62.74 kB 62.98 kB +0.57% 15.67 kB 15.76 kB
oss-experimental/react-art/cjs/react-art.development.js = 616.53 kB 608.90 kB = 97.85 kB 96.98 kB
oss-experimental/react-art/cjs/react-art.production.js = 324.85 kB 318.06 kB = 54.96 kB 54.13 kB

Generated by 🚫 dangerJS against 143c7f8

…etes

This ensures that we are measuring things in the viewport after scroll
restoration happens. Otherwise we end up with the wrong assumptions about
what will end up in the viewport.
In this case we flush the layout effects.

We also used to incorrect flush after in the wrong order.
@sebmarkbage sebmarkbage merged commit 800c9db into facebook:main Jan 8, 2025
github-actions Bot pushed a commit that referenced this pull request Jan 9, 2025
This adds navigation support to the View Transition fixture using both
`history.pushState/popstate` and the Navigation API models.

Because `popstate` does scroll restoration synchronously at the end of
the event, but `startViewTransition` cannot start synchronously, it
would observe the "old" state as after applying scroll restoration. This
leads to weird artifacts. So we intentionally do not support View
Transitions in `popstate`. If it suspends anyway for some other reason,
then scroll restoration is broken anyway and then it is supported. We
don't have to do anything here because this is already how things worked
because the sync `popstate` special case already included the sync lane
which opts it out of View Transitions.

For the Navigation API, scroll restoration can be blocked. The best way
to do this is to resolve the Navigation API promise after React has
applied its mutation. We can detect if there's currently any pending
navigation and wait to resolve the `startViewTransition` until it
finishes and any scroll restoration has been applied.

https://github.com/user-attachments/assets/f53b3282-6315-4513-b3d6-b8981d66964e

There is a subtle thing here. If we read the viewport metrics before
scroll restoration has been applied, then we might assume something is
or isn't going to be within the viewport incorrectly. This is evident on
the "Slide In from Left" example. When we're going forward to that page
we shift the scroll position such that it's going to appear in the
viewport. If we did this before applying scroll restoration, it would
not animate because it wasn't in the viewport then. Therefore, we need
to run the after mutation phase after scroll restoration.

A consequence of this is that you have to resolve Navigation in
`useInsertionEffect` as otherwise it leads to a deadlock (which
eventually gets broken by `startViewTransition`'s timeout of 10
seconds). Another consequence is that now `useLayoutEffect` observes the
restored state. However, I think what we'll likely do is move the layout
phase to before the after mutation phase which also ensures that
auto-scrolling inside `useLayoutEffect` are considered in the viewport
measurements as well.

DiffTrain build for [800c9db](800c9db)
github-actions Bot pushed a commit that referenced this pull request Jan 9, 2025
This adds navigation support to the View Transition fixture using both
`history.pushState/popstate` and the Navigation API models.

Because `popstate` does scroll restoration synchronously at the end of
the event, but `startViewTransition` cannot start synchronously, it
would observe the "old" state as after applying scroll restoration. This
leads to weird artifacts. So we intentionally do not support View
Transitions in `popstate`. If it suspends anyway for some other reason,
then scroll restoration is broken anyway and then it is supported. We
don't have to do anything here because this is already how things worked
because the sync `popstate` special case already included the sync lane
which opts it out of View Transitions.

For the Navigation API, scroll restoration can be blocked. The best way
to do this is to resolve the Navigation API promise after React has
applied its mutation. We can detect if there's currently any pending
navigation and wait to resolve the `startViewTransition` until it
finishes and any scroll restoration has been applied.

https://github.com/user-attachments/assets/f53b3282-6315-4513-b3d6-b8981d66964e

There is a subtle thing here. If we read the viewport metrics before
scroll restoration has been applied, then we might assume something is
or isn't going to be within the viewport incorrectly. This is evident on
the "Slide In from Left" example. When we're going forward to that page
we shift the scroll position such that it's going to appear in the
viewport. If we did this before applying scroll restoration, it would
not animate because it wasn't in the viewport then. Therefore, we need
to run the after mutation phase after scroll restoration.

A consequence of this is that you have to resolve Navigation in
`useInsertionEffect` as otherwise it leads to a deadlock (which
eventually gets broken by `startViewTransition`'s timeout of 10
seconds). Another consequence is that now `useLayoutEffect` observes the
restored state. However, I think what we'll likely do is move the layout
phase to before the after mutation phase which also ensures that
auto-scrolling inside `useLayoutEffect` are considered in the viewport
measurements as well.

DiffTrain build for [800c9db](800c9db)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants