-
Notifications
You must be signed in to change notification settings - Fork 46.1k
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
[Partial Hydration] Attempt hydration at a higher pri first if props/context changes #16352
Changes from all commits
ac5f5ec
80557ba
a41cf09
e4b74db
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 |
---|---|---|
|
@@ -171,7 +171,9 @@ import { | |
import { | ||
markSpawnedWork, | ||
requestCurrentTime, | ||
retryTimedOutBoundary, | ||
retryDehydratedSuspenseBoundary, | ||
scheduleWork, | ||
renderDidSuspendDelayIfPossible, | ||
} from './ReactFiberWorkLoop'; | ||
|
||
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; | ||
|
@@ -1476,6 +1478,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { | |
|
||
const SUSPENDED_MARKER: SuspenseState = { | ||
dehydrated: null, | ||
retryTime: Never, | ||
}; | ||
|
||
function shouldRemainOnFallback( | ||
|
@@ -1672,6 +1675,7 @@ function updateSuspenseComponent( | |
current, | ||
workInProgress, | ||
dehydrated, | ||
prevState, | ||
renderExpirationTime, | ||
); | ||
} else if ( | ||
|
@@ -2004,6 +2008,7 @@ function updateDehydratedSuspenseComponent( | |
current: Fiber, | ||
workInProgress: Fiber, | ||
suspenseInstance: SuspenseInstance, | ||
suspenseState: SuspenseState, | ||
renderExpirationTime: ExpirationTime, | ||
): null | Fiber { | ||
// We should never be hydrating at this point because it is the first pass, | ||
|
@@ -2033,11 +2038,31 @@ function updateDehydratedSuspenseComponent( | |
const hasContextChanged = current.childExpirationTime >= renderExpirationTime; | ||
if (didReceiveUpdate || hasContextChanged) { | ||
// This boundary has changed since the first render. This means that we are now unable to | ||
// hydrate it. We might still be able to hydrate it using an earlier expiration time but | ||
// during this render we can't. Instead, we're going to delete the whole subtree and | ||
// instead inject a new real Suspense boundary to take its place, which may render content | ||
// or fallback. The real Suspense boundary will suspend for a while so we have some time | ||
// to ensure it can produce real content, but all state and pending events will be lost. | ||
// hydrate it. We might still be able to hydrate it using an earlier expiration time, if | ||
// we are rendering at lower expiration than sync. | ||
if (renderExpirationTime < Sync) { | ||
if (suspenseState.retryTime <= renderExpirationTime) { | ||
// This render is even higher pri than we've seen before, let's try again | ||
// at even higher pri. | ||
let attemptHydrationAtExpirationTime = renderExpirationTime + 1; | ||
suspenseState.retryTime = attemptHydrationAtExpirationTime; | ||
scheduleWork(current, attemptHydrationAtExpirationTime); | ||
// TODO: Early abort this render. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @acdlite We don't currently have a way to abort a render while inside the work loop. I attempted something like sebmarkbage@c9c6c0a but I don't think that's the right approach. This works fine as long as we yield every 5ms anyway but it's not ideal when there are longer work loop times e.g. due to isInputPending or similar. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could throw something and then handle the interruption in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about that and I don't think I should call cc @bvaughn |
||
} else { | ||
// We have already tried to ping at a higher priority than we're rendering with | ||
// so if we got here, we must have failed to hydrate at those levels. We must | ||
// now give up. Instead, we're going to delete the whole subtree and instead inject | ||
// a new real Suspense boundary to take its place, which may render content | ||
// or fallback. This might suspend for a while and if it does we might still have | ||
// an opportunity to hydrate before this pass commits. | ||
} | ||
} | ||
// If we have scheduled higher pri work above, this will probably just abort the render | ||
// since we now have higher priority work, but in case it doesn't, we need to prepare to | ||
// render something, if we time out. Even if that requires us to delete everything and | ||
// skip hydration. | ||
// Delay having to do this as long as the suspense timeout allows us. | ||
renderDidSuspendDelayIfPossible(); | ||
return retrySuspenseComponentWithoutHydrating( | ||
current, | ||
workInProgress, | ||
|
@@ -2059,7 +2084,7 @@ function updateDehydratedSuspenseComponent( | |
// Register a callback to retry this boundary once the server has sent the result. | ||
registerSuspenseInstanceRetry( | ||
suspenseInstance, | ||
retryTimedOutBoundary.bind(null, current), | ||
retryDehydratedSuspenseBoundary.bind(null, current), | ||
); | ||
return null; | ||
} else { | ||
|
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.
To run this fixture you need to turn on enableSuspenseServerRenderer in ReactFeatureFlags.