mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Selective Hydration] ReactDOM.unstable_scheduleHydration(domNode) (#17004)
Adds an API to explicitly prioritize hydrating the path to a particular DOM node without relying on events to do it. The API uses the current scheduler priority to schedule it. For the same priority, the last one wins. This allows a similar effect as continuous events. This is useful for example to hydrate based on scroll position, or prioritize components that will upgrade to client-rendered-only content. I considered having an API that explicitly overrides the current target(s). However that makes it difficult to coordinate across components in an app. This just hydrates one target at a time but if it is blocked on I/O we could consider increasing priority of later targets too.
This commit is contained in:
@@ -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 <span>{text}</span>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
Scheduler.unstable_yieldValue('App');
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="A" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="B" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="C" />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
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(<App />);
|
||||
|
||||
// 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']);
|
||||
});
|
||||
});
|
||||
|
||||
+11
-1
@@ -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.
|
||||
|
||||
+107
-1
@@ -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<number, QueuedReplayableEvent> = new Map();
|
||||
let queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
|
||||
// We could consider replaying selectionchange and touchmoves too.
|
||||
|
||||
type QueuedHydrationTarget = {|
|
||||
blockedOn: null | Container | SuspenseInstance,
|
||||
target: Node,
|
||||
priority: number,
|
||||
|};
|
||||
let queuedExplicitHydrationTargets: Array<QueuedHydrationTarget> = [];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user