diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 5c238f88b6592..968779d23ff92 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -77,6 +77,7 @@ describe('ReactDOMServerPartialHydration', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableSuspenseCallback = true; ReactFeatureFlags.enableFlareAPI = true; + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; React = require('react'); ReactDOM = require('react-dom'); @@ -2475,4 +2476,85 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); + + it('finishes normal pri work before continuing to hydrate a retry', async () => { + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + let ref = React.createRef(); + + function Child() { + if (suspend) { + throw promise; + } else { + Scheduler.unstable_yieldValue('Child'); + return 'Hello'; + } + } + + function Sibling() { + Scheduler.unstable_yieldValue('Sibling'); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Commit Sibling'); + }); + return 'World'; + } + + // Avoid rerendering the tree by hoisting it. + const tree = ( + + + + + + ); + + function App({showSibling}) { + return ( +
+ {tree} + {showSibling ? : null} +
+ ); + } + + suspend = false; + let finalHTML = ReactDOMServer.renderToString(); + expect(Scheduler).toHaveYielded(['Child']); + + let container = document.createElement('div'); + container.innerHTML = finalHTML; + + suspend = true; + let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + expect(Scheduler).toFlushAndYield([]); + + expect(ref.current).toBe(null); + expect(container.textContent).toBe('Hello'); + + // Resolving the promise should continue hydration + suspend = false; + resolve(); + await promise; + + Scheduler.unstable_advanceTime(100); + + // Before we have a chance to flush it, we'll also render an update. + root.render(); + + // When we flush we expect the Normal pri render to take priority + // over hydration. + expect(Scheduler).toFlushAndYieldThrough(['Sibling', 'Commit Sibling']); + + // We shouldn't have hydrated the child yet. + expect(ref.current).toBe(null); + // But we did have a chance to update the content. + expect(container.textContent).toBe('HelloWorld'); + + expect(Scheduler).toFlushAndYield(['Child']); + + // Now we're hydrated. + expect(ref.current).not.toBe(null); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 675d444f3ac05..a6855d9b90bbe 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1482,7 +1482,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { const SUSPENDED_MARKER: SuspenseState = { dehydrated: null, - retryTime: Never, + retryTime: NoWork, }; function shouldRemainOnFallback( diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index dfb6964d64e28..4d29159d270cb 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -35,7 +35,9 @@ export type SuspenseState = {| // to check things like isSuspenseInstancePending. dehydrated: null | SuspenseInstance, // Represents the earliest expiration time we should attempt to hydrate - // a dehydrated boundary at. Never is the default. + // a dehydrated boundary at. + // Never is the default for dehydrated boundaries. + // NoWork is the default for normal boundaries, which turns into "normal" pri. retryTime: ExpirationTime, |}; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 0ff8ebe047699..63c7eefca591d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -741,17 +741,19 @@ function finishConcurrentRender( // statement, but eslint doesn't know about invariant, so it complains // if I do. eslint-disable-next-line no-fallthrough case RootErrored: { - if (expirationTime !== Idle) { - // If this was an async render, the error may have happened due to - // a mutation in a concurrent event. Try rendering one more time, - // synchronously, to see if the error goes away. If there are - // lower priority updates, let's include those, too, in case they - // fix the inconsistency. Render at Idle to include all updates. - markRootExpiredAtTime(root, Idle); - break; - } - // Commit the root in its errored state. - commitRoot(root); + // If this was an async render, the error may have happened due to + // a mutation in a concurrent event. Try rendering one more time, + // synchronously, to see if the error goes away. If there are + // lower priority updates, let's include those, too, in case they + // fix the inconsistency. Render at Idle to include all updates. + // If it was Idle or Never or some not-yet-invented time, render + // at that time. + markRootExpiredAtTime( + root, + expirationTime > Idle ? Idle : expirationTime, + ); + // We assume that this second render pass will be synchronous + // and therefore not hit this path again. break; } case RootSuspended: { @@ -2376,7 +2378,7 @@ function retryTimedOutBoundary( // previously was rendered in its fallback state. One of the promises that // suspended it has resolved, which means at least part of the tree was // likely unblocked. Try rendering again, at a new expiration time. - if (retryTime === Never) { + if (retryTime === NoWork) { const suspenseConfig = null; // Retries don't carry over the already committed update. const currentTime = requestCurrentTime(); retryTime = computeExpirationForFiber( @@ -2395,7 +2397,7 @@ function retryTimedOutBoundary( export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) { const suspenseState: null | SuspenseState = boundaryFiber.memoizedState; - let retryTime = Never; + let retryTime = NoWork; if (suspenseState !== null) { retryTime = suspenseState.retryTime; } @@ -2403,7 +2405,7 @@ export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) { } export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { - let retryTime = Never; // Default + let retryTime = NoWork; // Default let retryCache: WeakSet | Set | null; if (enableSuspenseServerRenderer) { switch (boundaryFiber.tag) {