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.