@samitouri / QOS-React-2 / commits / 3a2b5f148d

[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.

Sebastian Markbåge committed Oct 3, 2019 at 14:48 UTC 3a2b5f148d450c69aab67f055fc441d294c23518
4 files changed +178 -2
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+48
@@ -440,4 +440,52 @@ describe('ReactDOMServerSelectiveHydration', () => {
440
441 document.body.removeChild(container);
442 });
443 +
444 + it('hydrates the last explicitly hydrated target at higher priority', async () => {
445 + function Child({text}) {
446 + Scheduler.unstable_yieldValue(text);
447 + return <span>{text}</span>;
448 + }
449 +
450 + function App() {
451 + Scheduler.unstable_yieldValue('App');
452 + return (
453 + <div>
454 + <Suspense fallback="Loading...">
455 + <Child text="A" />
456 + </Suspense>
457 + <Suspense fallback="Loading...">
458 + <Child text="B" />
459 + </Suspense>
460 + <Suspense fallback="Loading...">
461 + <Child text="C" />
462 + </Suspense>
463 + </div>
464 + );
465 + }
466 +
467 + let finalHTML = ReactDOMServer.renderToString(<App />);
468 +
469 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C']);
470 +
471 + let container = document.createElement('div');
472 + container.innerHTML = finalHTML;
473 +
474 + let spanB = container.getElementsByTagName('span')[1];
475 + let spanC = container.getElementsByTagName('span')[2];
476 +
477 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
478 + root.render(<App />);
479 +
480 + // Nothing has been hydrated so far.
481 + expect(Scheduler).toHaveYielded([]);
482 +
483 + // Increase priority of B and then C.
484 + ReactDOM.unstable_scheduleHydration(spanB);
485 + ReactDOM.unstable_scheduleHydration(spanC);
486 +
487 + // We should prioritize hydrating C first because the last added
488 + // gets highest priority followed by the next added.
489 + expect(Scheduler).toFlushAndYield(['App', 'C', 'B', 'A']);
490 + });
491 });
packages/react-dom/src/client/ReactDOM.js
+11 -1
@@ -42,6 +42,7 @@ import {
42 attemptSynchronousHydration,
43 attemptUserBlockingHydration,
44 attemptContinuousHydration,
45 + attemptHydrationAtCurrentPriority,
46 } from 'react-reconciler/inline.dom';
47 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
48 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -81,8 +82,10 @@ import {
82 setAttemptSynchronousHydration,
83 setAttemptUserBlockingHydration,
84 setAttemptContinuousHydration,
85 + setAttemptHydrationAtCurrentPriority,
86 + eagerlyTrapReplayableEvents,
87 + queueExplicitHydrationTarget,
88 } from '../events/ReactDOMEventReplaying';
85 -import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
89 import {
90 ELEMENT_NODE,
91 COMMENT_NODE,
@@ -94,6 +97,7 @@ import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
97 setAttemptSynchronousHydration(attemptSynchronousHydration);
98 setAttemptUserBlockingHydration(attemptUserBlockingHydration);
99 setAttemptContinuousHydration(attemptContinuousHydration);
100 +setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority);
101
102 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
103
@@ -841,6 +845,12 @@ const ReactDOM: Object = {
845 unstable_createSyncRoot: createSyncRoot,
846 unstable_flushControlled: flushControlled,
847
848 + unstable_scheduleHydration(target: Node) {
849 + if (target) {
850 + queueExplicitHydrationTarget(target);
851 + }
852 + },
853 +
854 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
855 // Keep in sync with ReactDOMUnstableNativeDependencies.js
856 // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
packages/react-dom/src/events/ReactDOMEventReplaying.js
+107 -1
@@ -11,15 +11,23 @@ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11 import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
12 import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
13 import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
14 +import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
15
16 import {
17 enableFlareAPI,
18 enableSelectiveHydration,
19 } from 'shared/ReactFeatureFlags';
20 import {
21 + unstable_runWithPriority as runWithPriority,
22 unstable_scheduleCallback as scheduleCallback,
23 unstable_NormalPriority as NormalPriority,
24 + unstable_getCurrentPriorityLevel as getCurrentPriorityLevel,
25 } from 'scheduler';
26 +import {
27 + getNearestMountedFiber,
28 + getContainerFromFiber,
29 + getSuspenseInstanceFromFiber,
30 +} from 'react-reconciler/reflection';
31 import {
32 attemptToDispatchEvent,
33 trapEventForResponderEventSystem,
@@ -28,8 +36,12 @@ import {
36 getListeningSetForElement,
37 listenToTopLevel,
38 } from './ReactBrowserEventEmitter';
31 -import {getInstanceFromNode} from '../client/ReactDOMComponentTree';
39 +import {
40 + getInstanceFromNode,
41 + getClosestInstanceFromNode,
42 +} from '../client/ReactDOMComponentTree';
43 import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';
44 +import {HostRoot, SuspenseComponent} from 'shared/ReactWorkTags';
45
46 let attemptSynchronousHydration: (fiber: Object) => void;
47
@@ -49,6 +61,14 @@ export function setAttemptContinuousHydration(fn: (fiber: Object) => void) {
61 attemptContinuousHydration = fn;
62 }
63
64 +let attemptHydrationAtCurrentPriority: (fiber: Object) => void;
65 +
66 +export function setAttemptHydrationAtCurrentPriority(
67 + fn: (fiber: Object) => void,
68 +) {
69 + attemptHydrationAtCurrentPriority = fn;
70 +}
71 +
72 // TODO: Upgrade this definition once we're on a newer version of Flow that
73 // has this definition built-in.
74 type PointerEvent = Event & {
@@ -124,6 +144,13 @@ let queuedPointers: Map<number, QueuedReplayableEvent> = new Map();
144 let queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
145 // We could consider replaying selectionchange and touchmoves too.
146
147 +type QueuedHydrationTarget = {|
148 + blockedOn: null | Container | SuspenseInstance,
149 + target: Node,
150 + priority: number,
151 +|};
152 +let queuedExplicitHydrationTargets: Array<QueuedHydrationTarget> = [];
153 +
154 export function hasQueuedDiscreteEvents(): boolean {
155 return queuedDiscreteEvents.length > 0;
156 }
@@ -422,6 +449,64 @@ export function queueIfContinuousEvent(
449 return false;
450 }
451
452 +// Check if this target is unblocked. Returns true if it's unblocked.
453 +function attemptExplicitHydrationTarget(
454 + queuedTarget: QueuedHydrationTarget,
455 +): void {
456 + // TODO: This function shares a lot of logic with attemptToDispatchEvent.
457 + // Try to unify them. It's a bit tricky since it would require two return
458 + // values.
459 + let targetInst = getClosestInstanceFromNode(queuedTarget.target);
460 + if (targetInst !== null) {
461 + let nearestMounted = getNearestMountedFiber(targetInst);
462 + if (nearestMounted !== null) {
463 + const tag = nearestMounted.tag;
464 + if (tag === SuspenseComponent) {
465 + let instance = getSuspenseInstanceFromFiber(nearestMounted);
466 + if (instance !== null) {
467 + // We're blocked on hydrating this boundary.
468 + // Increase its priority.
469 + queuedTarget.blockedOn = instance;
470 + runWithPriority(queuedTarget.priority, () => {
471 + attemptHydrationAtCurrentPriority(nearestMounted);
472 + });
473 + return;
474 + }
475 + } else if (tag === HostRoot) {
476 + const root: FiberRoot = nearestMounted.stateNode;
477 + if (root.hydrate) {
478 + queuedTarget.blockedOn = getContainerFromFiber(nearestMounted);
479 + // We don't currently have a way to increase the priority of
480 + // a root other than sync.
481 + return;
482 + }
483 + }
484 + }
485 + }
486 + queuedTarget.blockedOn = null;
487 +}
488 +
489 +export function queueExplicitHydrationTarget(target: Node): void {
490 + if (enableSelectiveHydration) {
491 + let priority = getCurrentPriorityLevel();
492 + const queuedTarget: QueuedHydrationTarget = {
493 + blockedOn: null,
494 + target: target,
495 + priority: priority,
496 + };
497 + let i = 0;
498 + for (; i < queuedExplicitHydrationTargets.length; i++) {
499 + if (priority <= queuedExplicitHydrationTargets[i].priority) {
500 + break;
501 + }
502 + }
503 + queuedExplicitHydrationTargets.splice(i, 0, queuedTarget);
504 + if (i === 0) {
505 + attemptExplicitHydrationTarget(queuedTarget);
506 + }
507 + }
508 +}
509 +
510 function attemptReplayContinuousQueuedEvent(
511 queuedEvent: QueuedReplayableEvent,
512 ): boolean {
@@ -544,4 +629,25 @@ export function retryIfBlockedOn(
629 scheduleCallbackIfUnblocked(queuedEvent, unblocked);
630 queuedPointers.forEach(unblock);
631 queuedPointerCaptures.forEach(unblock);
632 +
633 + for (let i = 0; i < queuedExplicitHydrationTargets.length; i++) {
634 + let queuedTarget = queuedExplicitHydrationTargets[i];
635 + if (queuedTarget.blockedOn === unblocked) {
636 + queuedTarget.blockedOn = null;
637 + }
638 + }
639 +
640 + while (queuedExplicitHydrationTargets.length > 0) {
641 + let nextExplicitTarget = queuedExplicitHydrationTargets[0];
642 + if (nextExplicitTarget.blockedOn !== null) {
643 + // We're still blocked.
644 + break;
645 + } else {
646 + attemptExplicitHydrationTarget(nextExplicitTarget);
647 + if (nextExplicitTarget.blockedOn === null) {
648 + // We're unblocked.
649 + queuedExplicitHydrationTargets.shift();
650 + }
651 + }
652 + }
653 }
packages/react-reconciler/src/ReactFiberReconciler.js
+12
@@ -438,6 +438,18 @@ export function attemptContinuousHydration(fiber: Fiber): void {
438 markRetryTimeIfNotHydrated(fiber, expTime);
439 }
440
441 +export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
442 + if (fiber.tag !== SuspenseComponent) {
443 + // We ignore HostRoots here because we can't increase
444 + // their priority other than synchronously flush it.
445 + return;
446 + }
447 + const currentTime = requestCurrentTime();
448 + const expTime = computeExpirationForFiber(currentTime, fiber, null);
449 + scheduleWork(fiber, expTime);
450 + markRetryTimeIfNotHydrated(fiber, expTime);
451 +}
452 +
453 export {findHostInstance};
454
455 export {findHostInstanceWithWarning};