diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js index 5cafd83fdd243..d21059ba6cf0b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js @@ -255,6 +255,39 @@ describe('ReactDOMFizzShellHydration', () => { }, ); + // @gate enableHydrationLaneScheduling + it( + 'updating the root at same priority as initial hydration does not ' + + 'force a client render', + async () => { + function App() { + return ; + } + + // Server render + await resolveText('Initial'); + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + assertLog(['Initial']); + + await clientAct(async () => { + let root; + startTransition(() => { + root = ReactDOMClient.hydrateRoot(container, ); + }); + // This has lower priority than the initial hydration, so the update + // won't be processed until after hydration finishes. + startTransition(() => { + root.render(); + }); + }); + assertLog(['Initial', 'Updated']); + expect(container.textContent).toBe('Updated'); + }, + ); + it('updating the root while the shell is suspended forces a client render', async () => { function App() { return ; diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index b98019046e3c2..ee3a0a3df2b60 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -995,61 +995,66 @@ export function getBumpedLaneForHydration( renderLanes: Lanes, ): Lane { const renderLane = getHighestPriorityLane(renderLanes); - - let lane; - if ((renderLane & SyncUpdateLanes) !== NoLane) { - lane = SyncHydrationLane; - } else { - switch (renderLane) { - case SyncLane: - lane = SyncHydrationLane; - break; - case InputContinuousLane: - lane = InputContinuousHydrationLane; - break; - case DefaultLane: - lane = DefaultHydrationLane; - break; - case TransitionLane1: - case TransitionLane2: - case TransitionLane3: - case TransitionLane4: - case TransitionLane5: - case TransitionLane6: - case TransitionLane7: - case TransitionLane8: - case TransitionLane9: - case TransitionLane10: - case TransitionLane11: - case TransitionLane12: - case TransitionLane13: - case TransitionLane14: - case TransitionLane15: - case RetryLane1: - case RetryLane2: - case RetryLane3: - case RetryLane4: - lane = TransitionHydrationLane; - break; - case IdleLane: - lane = IdleHydrationLane; - break; - default: - // Everything else is already either a hydration lane, or shouldn't - // be retried at a hydration lane. - lane = NoLane; - break; - } - } - + const bumpedLane = + (renderLane & SyncUpdateLanes) !== NoLane + ? // Unify sync lanes. We don't do this inside getBumpedLaneForHydrationByLane + // because that causes things to flush synchronously when they shouldn't. + // TODO: This is not coherent but that's beacuse the unification is not coherent. + // We need to get merge these into an actual single lane. + SyncHydrationLane + : getBumpedLaneForHydrationByLane(renderLane); // Check if the lane we chose is suspended. If so, that indicates that we // already attempted and failed to hydrate at that level. Also check if we're // already rendering that lane, which is rare but could happen. - if ((lane & (root.suspendedLanes | renderLanes)) !== NoLane) { + // TODO: This should move into the caller to decide whether giving up is valid. + if ((bumpedLane & (root.suspendedLanes | renderLanes)) !== NoLane) { // Give up trying to hydrate and fall back to client render. return NoLane; } + return bumpedLane; +} +export function getBumpedLaneForHydrationByLane(lane: Lane): Lane { + switch (lane) { + case SyncLane: + lane = SyncHydrationLane; + break; + case InputContinuousLane: + lane = InputContinuousHydrationLane; + break; + case DefaultLane: + lane = DefaultHydrationLane; + break; + case TransitionLane1: + case TransitionLane2: + case TransitionLane3: + case TransitionLane4: + case TransitionLane5: + case TransitionLane6: + case TransitionLane7: + case TransitionLane8: + case TransitionLane9: + case TransitionLane10: + case TransitionLane11: + case TransitionLane12: + case TransitionLane13: + case TransitionLane14: + case TransitionLane15: + case RetryLane1: + case RetryLane2: + case RetryLane3: + case RetryLane4: + lane = TransitionHydrationLane; + break; + case IdleLane: + lane = IdleHydrationLane; + break; + default: + // Everything else is already either a hydration lane, or shouldn't + // be retried at a hydration lane. + lane = NoLane; + break; + } return lane; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 31a7fbeccf36b..98ef2ef93fc46 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -38,7 +38,10 @@ import { } from './ReactWorkTags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import isArray from 'shared/isArray'; -import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags'; +import { + enableSchedulingProfiler, + enableHydrationLaneScheduling, +} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { getPublicInstance, @@ -91,6 +94,7 @@ import { SelectiveHydrationLane, getHighestPriorityPendingLanes, higherPriorityLane, + getBumpedLaneForHydrationByLane, } from './ReactFiberLane'; import { scheduleRefresh, @@ -322,7 +326,10 @@ export function createHydrationContainer( // the update to schedule work on the root fiber (and, for legacy roots, to // enqueue the callback if one is provided). const current = root.current; - const lane = requestUpdateLane(current); + let lane = requestUpdateLane(current); + if (enableHydrationLaneScheduling) { + lane = getBumpedLaneForHydrationByLane(lane); + } const update = createUpdate(lane); update.callback = callback !== undefined && callback !== null ? callback : null; @@ -533,7 +540,10 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { // their priority other than synchronously flush it. return; } - const lane = requestUpdateLane(fiber); + let lane = requestUpdateLane(fiber); + if (enableHydrationLaneScheduling) { + lane = getBumpedLaneForHydrationByLane(lane); + } const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { scheduleUpdateOnFiber(root, fiber, lane); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a53d911d8b151..6ccf1ce8dd828 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -116,7 +116,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = __EXPERIMENTAL__; -export const enableHydrationLaneScheduling = __EXPERIMENTAL__; +export const enableHydrationLaneScheduling = true; // Enables useMemoCache hook, intended as a compilation target for // auto-memoization.