-
Notifications
You must be signed in to change notification settings - Fork 49.8k
[react-dom] Enforce small gap between completed navigation and default Transition indicator #33354
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 'use client'; | ||
|
|
||
| import * as React from 'react'; | ||
| import Container from './Container.js'; | ||
|
|
||
| export function Navigate() { | ||
| /** Repro for https://issues.chromium.org/u/1/issues/419746417 */ | ||
| function provokeChromeCrash() { | ||
| React.startTransition(async () => { | ||
| console.log('Default transition triggered'); | ||
|
|
||
| await new Promise(resolve => { | ||
| setTimeout( | ||
| () => { | ||
| history.pushState( | ||
| {}, | ||
| '', | ||
| `?chrome-crash-419746417=${performance.now()}` | ||
| ); | ||
| }, | ||
| // This needs to happen before React's default transition indicator | ||
| // is displayed but after it's scheduled. | ||
| 100 + -50 | ||
| ); | ||
|
|
||
| setTimeout(() => { | ||
| console.log('Default transition completed'); | ||
| resolve(); | ||
| }, 1000); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <Container> | ||
| <h2>Navigation fixture</h2> | ||
| <button onClick={provokeChromeCrash}>Provoke Chrome Crash (fixed)</button> | ||
| </Container> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ | |
| * @flow | ||
| */ | ||
|
|
||
| const defaultTransitionIndicatorDelay = 100; | ||
|
|
||
| export function defaultOnDefaultTransitionIndicator(): void | (() => void) { | ||
| if (typeof navigation !== 'object') { | ||
| // If the Navigation API is not available, then this is a noop. | ||
|
|
@@ -16,6 +18,10 @@ export function defaultOnDefaultTransitionIndicator(): void | (() => void) { | |
| let isCancelled = false; | ||
| let pendingResolve: null | (() => void) = null; | ||
|
|
||
| const scheduledAt = performance.now(); | ||
| // Delay the start a bit in case this is a fast Transition. | ||
| setTimeout(startFakeNavigation, defaultTransitionIndicatorDelay); | ||
|
|
||
| function handleNavigate(event: NavigateEvent) { | ||
| if (event.canIntercept && event.info === 'react-transition') { | ||
| event.intercept({ | ||
|
|
@@ -29,6 +35,7 @@ export function defaultOnDefaultTransitionIndicator(): void | (() => void) { | |
| } | ||
|
|
||
| function handleNavigateComplete() { | ||
| const wasRunning = pendingResolve !== null; | ||
| if (pendingResolve !== null) { | ||
| // If this was not our navigation completing, we were probably cancelled. | ||
| // We'll start a new one below. | ||
|
|
@@ -38,7 +45,18 @@ export function defaultOnDefaultTransitionIndicator(): void | (() => void) { | |
| if (!isCancelled) { | ||
| // Some other navigation completed but we should still be running. | ||
| // Start another fake one to keep the loading indicator going. | ||
| startFakeNavigation(); | ||
| if (wasRunning) { | ||
| // Restart sync to make it not janky if it was already running | ||
| startFakeNavigation(); | ||
| } else { | ||
| // Since it hasn't started yet, we want to delay it up to a bit. | ||
| // There needs to be an async gap to work around https://issues.chromium.org/u/1/issues/419746417. | ||
| const fakeNavigationDelay = Math.max( | ||
|
||
| 0, | ||
| defaultTransitionIndicatorDelay - (performance.now() - scheduledAt), | ||
|
||
| ); | ||
| setTimeout(startFakeNavigation, fakeNavigationDelay); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -70,9 +88,6 @@ export function defaultOnDefaultTransitionIndicator(): void | (() => void) { | |
| } | ||
| } | ||
|
|
||
| // Delay the start a bit in case this is a fast navigation. | ||
| setTimeout(startFakeNavigation, 100); | ||
|
|
||
| return function () { | ||
| isCancelled = true; | ||
| // $FlowFixMe | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this cause the Chrome bug to happen anyway in this scenario?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to only crash if we start the spinner from navigationcomplete triggered by
replaceState. If the spinner is already running, starting the fake sync navigation is fine. Probably because the earlierpendingResolvecall will only stop the spinner in the next task?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's weird. I wonder if there's a race condition though that
Regardless, doesn't this trigger the spinner to restart? So maybe it's worth waiting a little to see if we get unblocked to avoid restarting it? Since it'll glitch regardless if we restart but not if we can avoid it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't actually for the case where another navigation completes before this Transition finished:
navigationcompletetriggers whilenavigation.transitionis still set even though it finishes right after innavigation.transition.finished.then. So we might have to add listeners to thenavigation.transition.finishedPromise as well.Anyway, the current implementation can't be used as it crashes Chrome. So while I would definitely wait for Chrome to acknowledge the issue before landing this in Canary, I'd still merge this so that we can at least continue experimenting with it.
In the meantime I come up with some more synthetic scenarios that increase our coverage