diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
index 0283b06ef2cd5..e9983df3468ab 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
@@ -440,4 +440,52 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
+
+ it('hydrates the last explicitly hydrated target at higher priority', async () => {
+ function Child({text}) {
+ Scheduler.unstable_yieldValue(text);
+ return {text};
+ }
+
+ function App() {
+ Scheduler.unstable_yieldValue('App');
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ let finalHTML = ReactDOMServer.renderToString();
+
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C']);
+
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ let spanB = container.getElementsByTagName('span')[1];
+ let spanC = container.getElementsByTagName('span')[2];
+
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
+
+ // Nothing has been hydrated so far.
+ expect(Scheduler).toHaveYielded([]);
+
+ // Increase priority of B and then C.
+ ReactDOM.unstable_scheduleHydration(spanB);
+ ReactDOM.unstable_scheduleHydration(spanC);
+
+ // We should prioritize hydrating C first because the last added
+ // gets highest priority followed by the next added.
+ expect(Scheduler).toFlushAndYield(['App', 'C', 'B', 'A']);
+ });
});
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 11c3f37e306d2..753601dbecdd2 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -42,6 +42,7 @@ import {
attemptSynchronousHydration,
attemptUserBlockingHydration,
attemptContinuousHydration,
+ attemptHydrationAtCurrentPriority,
} from 'react-reconciler/inline.dom';
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -81,8 +82,10 @@ import {
setAttemptSynchronousHydration,
setAttemptUserBlockingHydration,
setAttemptContinuousHydration,
+ setAttemptHydrationAtCurrentPriority,
+ eagerlyTrapReplayableEvents,
+ queueExplicitHydrationTarget,
} from '../events/ReactDOMEventReplaying';
-import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
COMMENT_NODE,
@@ -94,6 +97,7 @@ import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
setAttemptSynchronousHydration(attemptSynchronousHydration);
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
setAttemptContinuousHydration(attemptContinuousHydration);
+setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority);
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -841,6 +845,12 @@ const ReactDOM: Object = {
unstable_createSyncRoot: createSyncRoot,
unstable_flushControlled: flushControlled,
+ unstable_scheduleHydration(target: Node) {
+ if (target) {
+ queueExplicitHydrationTarget(target);
+ }
+ },
+
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
// Keep in sync with ReactDOMUnstableNativeDependencies.js
// ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js
index 53d1a347d6357..f8d0c81885a28 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -11,15 +11,23 @@ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
import {
enableFlareAPI,
enableSelectiveHydration,
} from 'shared/ReactFeatureFlags';
import {
+ unstable_runWithPriority as runWithPriority,
unstable_scheduleCallback as scheduleCallback,
unstable_NormalPriority as NormalPriority,
+ unstable_getCurrentPriorityLevel as getCurrentPriorityLevel,
} from 'scheduler';
+import {
+ getNearestMountedFiber,
+ getContainerFromFiber,
+ getSuspenseInstanceFromFiber,
+} from 'react-reconciler/reflection';
import {
attemptToDispatchEvent,
trapEventForResponderEventSystem,
@@ -28,8 +36,12 @@ import {
getListeningSetForElement,
listenToTopLevel,
} from './ReactBrowserEventEmitter';
-import {getInstanceFromNode} from '../client/ReactDOMComponentTree';
+import {
+ getInstanceFromNode,
+ getClosestInstanceFromNode,
+} from '../client/ReactDOMComponentTree';
import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';
+import {HostRoot, SuspenseComponent} from 'shared/ReactWorkTags';
let attemptSynchronousHydration: (fiber: Object) => void;
@@ -49,6 +61,14 @@ export function setAttemptContinuousHydration(fn: (fiber: Object) => void) {
attemptContinuousHydration = fn;
}
+let attemptHydrationAtCurrentPriority: (fiber: Object) => void;
+
+export function setAttemptHydrationAtCurrentPriority(
+ fn: (fiber: Object) => void,
+) {
+ attemptHydrationAtCurrentPriority = fn;
+}
+
// TODO: Upgrade this definition once we're on a newer version of Flow that
// has this definition built-in.
type PointerEvent = Event & {
@@ -124,6 +144,13 @@ let queuedPointers: Map = new Map();
let queuedPointerCaptures: Map = new Map();
// We could consider replaying selectionchange and touchmoves too.
+type QueuedHydrationTarget = {|
+ blockedOn: null | Container | SuspenseInstance,
+ target: Node,
+ priority: number,
+|};
+let queuedExplicitHydrationTargets: Array = [];
+
export function hasQueuedDiscreteEvents(): boolean {
return queuedDiscreteEvents.length > 0;
}
@@ -422,6 +449,64 @@ export function queueIfContinuousEvent(
return false;
}
+// Check if this target is unblocked. Returns true if it's unblocked.
+function attemptExplicitHydrationTarget(
+ queuedTarget: QueuedHydrationTarget,
+): void {
+ // TODO: This function shares a lot of logic with attemptToDispatchEvent.
+ // Try to unify them. It's a bit tricky since it would require two return
+ // values.
+ let targetInst = getClosestInstanceFromNode(queuedTarget.target);
+ if (targetInst !== null) {
+ let nearestMounted = getNearestMountedFiber(targetInst);
+ if (nearestMounted !== null) {
+ const tag = nearestMounted.tag;
+ if (tag === SuspenseComponent) {
+ let instance = getSuspenseInstanceFromFiber(nearestMounted);
+ if (instance !== null) {
+ // We're blocked on hydrating this boundary.
+ // Increase its priority.
+ queuedTarget.blockedOn = instance;
+ runWithPriority(queuedTarget.priority, () => {
+ attemptHydrationAtCurrentPriority(nearestMounted);
+ });
+ return;
+ }
+ } else if (tag === HostRoot) {
+ const root: FiberRoot = nearestMounted.stateNode;
+ if (root.hydrate) {
+ queuedTarget.blockedOn = getContainerFromFiber(nearestMounted);
+ // We don't currently have a way to increase the priority of
+ // a root other than sync.
+ return;
+ }
+ }
+ }
+ }
+ queuedTarget.blockedOn = null;
+}
+
+export function queueExplicitHydrationTarget(target: Node): void {
+ if (enableSelectiveHydration) {
+ let priority = getCurrentPriorityLevel();
+ const queuedTarget: QueuedHydrationTarget = {
+ blockedOn: null,
+ target: target,
+ priority: priority,
+ };
+ let i = 0;
+ for (; i < queuedExplicitHydrationTargets.length; i++) {
+ if (priority <= queuedExplicitHydrationTargets[i].priority) {
+ break;
+ }
+ }
+ queuedExplicitHydrationTargets.splice(i, 0, queuedTarget);
+ if (i === 0) {
+ attemptExplicitHydrationTarget(queuedTarget);
+ }
+ }
+}
+
function attemptReplayContinuousQueuedEvent(
queuedEvent: QueuedReplayableEvent,
): boolean {
@@ -544,4 +629,25 @@ export function retryIfBlockedOn(
scheduleCallbackIfUnblocked(queuedEvent, unblocked);
queuedPointers.forEach(unblock);
queuedPointerCaptures.forEach(unblock);
+
+ for (let i = 0; i < queuedExplicitHydrationTargets.length; i++) {
+ let queuedTarget = queuedExplicitHydrationTargets[i];
+ if (queuedTarget.blockedOn === unblocked) {
+ queuedTarget.blockedOn = null;
+ }
+ }
+
+ while (queuedExplicitHydrationTargets.length > 0) {
+ let nextExplicitTarget = queuedExplicitHydrationTargets[0];
+ if (nextExplicitTarget.blockedOn !== null) {
+ // We're still blocked.
+ break;
+ } else {
+ attemptExplicitHydrationTarget(nextExplicitTarget);
+ if (nextExplicitTarget.blockedOn === null) {
+ // We're unblocked.
+ queuedExplicitHydrationTargets.shift();
+ }
+ }
+ }
}
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 00d2b14ae6389..710c8f054a954 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -438,6 +438,18 @@ export function attemptContinuousHydration(fiber: Fiber): void {
markRetryTimeIfNotHydrated(fiber, expTime);
}
+export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
+ if (fiber.tag !== SuspenseComponent) {
+ // We ignore HostRoots here because we can't increase
+ // their priority other than synchronously flush it.
+ return;
+ }
+ const currentTime = requestCurrentTime();
+ const expTime = computeExpirationForFiber(currentTime, fiber, null);
+ scheduleWork(fiber, expTime);
+ markRetryTimeIfNotHydrated(fiber, expTime);
+}
+
export {findHostInstance};
export {findHostInstanceWithWarning};