Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Selective Hydration] ReactDOM.unstable_scheduleHydration(domNode) #17004

Merged
merged 1 commit into from Oct 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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']);
});
});
12 changes: 11 additions & 1 deletion packages/react-dom/src/client/ReactDOM.js
Expand Up @@ -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';
Expand Down Expand Up @@ -81,8 +82,10 @@ import {
setAttemptSynchronousHydration,
setAttemptUserBlockingHydration,
setAttemptContinuousHydration,
setAttemptHydrationAtCurrentPriority,
eagerlyTrapReplayableEvents,
queueExplicitHydrationTarget,
} from '../events/ReactDOMEventReplaying';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
COMMENT_NODE,
Expand All @@ -94,6 +97,7 @@ import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
setAttemptSynchronousHydration(attemptSynchronousHydration);
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
setAttemptContinuousHydration(attemptContinuousHydration);
setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority);

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -841,6 +845,12 @@ const ReactDOM: Object = {
unstable_createSyncRoot: createSyncRoot,
unstable_flushControlled: flushControlled,

unstable_scheduleHydration(target: Node) {
if (target) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason behind silently nooping if we're passed an invalid target?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because it's pretty common to pass in getElementById and probably not worth taking down the site since this feature has no important semantic meaning other than perf. It should probably be a warning at some point though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seems like we should at least warn since the overall scheduling behavior is a bit opaque to begin with. (Might be easy to overlook an error like a misspelled id.)

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.
Expand Down
108 changes: 107 additions & 1 deletion packages/react-dom/src/events/ReactDOMEventReplaying.js
Expand Up @@ -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,
Expand All @@ -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;

Expand All @@ -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 & {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
}
}
}
12 changes: 12 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Expand Up @@ -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};
Expand Down