Skip to content

Commit

Permalink
Selective Hydration (#16880)
Browse files Browse the repository at this point in the history
* Add Feature Flag for Selective Hydration

* Enable Synchronous Hydration of Discrete Events

* Resolve cyclic dependency
  • Loading branch information
sebmarkbage committed Sep 25, 2019
1 parent 4bb0e96 commit 3694a3b
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 16 deletions.
@@ -0,0 +1,117 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;
let ReactFeatureFlags;
let Suspense;

function dispatchClickEvent(target) {
const mouseOutEvent = document.createEvent('MouseEvents');
mouseOutEvent.initMouseEvent(
'click',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
target,
);
return target.dispatchEvent(mouseOutEvent);
}

describe('ReactDOMServerSelectiveHydration', () => {
beforeEach(() => {
jest.resetModuleRegistry();

ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
ReactFeatureFlags.enableSelectiveHydration = true;

React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
Suspense = React.Suspense;
});

it('hydrates the target boundary synchronously during a click', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
return (
<span
onClick={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Clicked ' + text);
}}>
{text}
</span>
);
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<div>
<Suspense fallback="Loading...">
<Child text="A" />
</Suspense>
<Suspense fallback="Loading...">
<Child text="B" />
</Suspense>
</div>
);
}

let finalHTML = ReactDOMServer.renderToString(<App />);

expect(Scheduler).toHaveYielded(['App', 'A', 'B']);

let container = document.createElement('div');
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);

container.innerHTML = finalHTML;

let span = container.getElementsByTagName('span')[1];

let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);

// Nothing has been hydrated so far.
expect(Scheduler).toHaveYielded([]);

// This should synchronously hydrate the root App and the second suspense
// boundary.
let result = dispatchClickEvent(span);

// The event should have been canceled because we called preventDefault.
expect(result).toBe(false);

// We rendered App, B and then invoked the event without rendering A.
expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);

// After continuing the scheduler, we finally hydrate A.
expect(Scheduler).toFlushAndYield(['A']);

document.body.removeChild(container);
});
});
4 changes: 4 additions & 0 deletions packages/react-dom/src/client/ReactDOM.js
Expand Up @@ -39,6 +39,7 @@ import {
findHostInstanceWithWarning,
flushPassiveEffects,
IsThisRendererActing,
attemptSynchronousHydration,
} from 'react-reconciler/inline.dom';
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
import {canUseDOM} from 'shared/ExecutionEnvironment';
Expand Down Expand Up @@ -74,6 +75,7 @@ import {
} from './ReactDOMComponentTree';
import {restoreControlledState} from './ReactDOMComponent';
import {dispatchEvent} from '../events/ReactDOMEventListener';
import {setAttemptSynchronousHydration} from '../events/ReactDOMEventReplaying';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
Expand All @@ -83,6 +85,8 @@ import {
} from '../shared/HTMLNodeType';
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';

setAttemptSynchronousHydration(attemptSynchronousHydration);

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

let topLevelUpdateWarnings;
Expand Down
16 changes: 13 additions & 3 deletions packages/react-dom/src/client/ReactDOMComponentTree.js
Expand Up @@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/

import {HostComponent, HostText} from 'shared/ReactWorkTags';
import {
HostComponent,
HostText,
HostRoot,
SuspenseComponent,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';

import {getParentSuspenseInstance} from './ReactDOMHostConfig';
Expand Down Expand Up @@ -112,9 +117,14 @@ export function getClosestInstanceFromNode(targetNode) {
* instance, or null if the node was not rendered by this React.
*/
export function getInstanceFromNode(node) {
const inst = node[internalInstanceKey];
const inst = node[internalInstanceKey] || node[internalContainerInstanceKey];
if (inst) {
if (inst.tag === HostComponent || inst.tag === HostText) {
if (
inst.tag === HostComponent ||
inst.tag === HostText ||
inst.tag === SuspenseComponent ||
inst.tag === HostRoot
) {
return inst;
} else {
return null;
Expand Down
52 changes: 40 additions & 12 deletions packages/react-dom/src/events/ReactDOMEventReplaying.js
Expand Up @@ -12,7 +12,10 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';

import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import {
enableFlareAPI,
enableSelectiveHydration,
} from 'shared/ReactFeatureFlags';
import {
unstable_scheduleCallback as scheduleCallback,
unstable_NormalPriority as NormalPriority,
Expand All @@ -25,8 +28,15 @@ import {
getListeningSetForElement,
listenToTopLevel,
} from './ReactBrowserEventEmitter';
import {getInstanceFromNode} from '../client/ReactDOMComponentTree';
import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';

let attemptSynchronousHydration: (fiber: Object) => void;

export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) {
attemptSynchronousHydration = 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 @@ -223,18 +233,36 @@ export function queueDiscreteEvent(
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
): void {
queuedDiscreteEvents.push(
createQueuedReplayableEvent(
blockedOn,
topLevelType,
eventSystemFlags,
nativeEvent,
),
const queuedEvent = createQueuedReplayableEvent(
blockedOn,
topLevelType,
eventSystemFlags,
nativeEvent,
);
if (blockedOn === null && queuedDiscreteEvents.length === 1) {
// This probably shouldn't happen but some defensive coding might
// help us get unblocked if we have a bug.
replayUnblockedEvents();
queuedDiscreteEvents.push(queuedEvent);
if (enableSelectiveHydration) {
if (queuedDiscreteEvents.length === 1) {
// If this was the first discrete event, we might be able to
// synchronously unblock it so that preventDefault still works.
while (queuedEvent.blockedOn !== null) {
let fiber = getInstanceFromNode(queuedEvent.blockedOn);
if (fiber === null) {
break;
}
attemptSynchronousHydration(fiber);
if (queuedEvent.blockedOn === null) {
// We got unblocked by hydration. Let's try again.
replayUnblockedEvents();
// If we're reblocked, on an inner boundary, we might need
// to attempt hydrating that one.
continue;
} else {
// We're still blocked from hydation, we have to give up
// and replay later.
break;
}
}
}
}
}

Expand Down
22 changes: 21 additions & 1 deletion packages/react-reconciler/src/ReactFiberReconciler.js
Expand Up @@ -27,7 +27,12 @@ import {
findCurrentHostFiberWithNoPortals,
} from 'react-reconciler/reflection';
import {get as getInstance} from 'shared/ReactInstanceMap';
import {HostComponent, ClassComponent} from 'shared/ReactWorkTags';
import {
HostComponent,
ClassComponent,
HostRoot,
SuspenseComponent,
} from 'shared/ReactWorkTags';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
Expand Down Expand Up @@ -362,6 +367,21 @@ export function getPublicRootInstance(
}
}

export function attemptSynchronousHydration(fiber: Fiber): void {
switch (fiber.tag) {
case HostRoot:
let root: FiberRoot = fiber.stateNode;
if (root.hydrate) {
// Flush the first scheduled "update".
flushRoot(root, root.firstPendingTime);
}
break;
case SuspenseComponent:
flushSync(() => scheduleWork(fiber, Sync));
break;
}
}

export {findHostInstance};

export {findHostInstanceWithWarning};
Expand Down
1 change: 1 addition & 0 deletions packages/shared/ReactFeatureFlags.js
Expand Up @@ -33,6 +33,7 @@ export const enableSchedulerTracing = __PROFILE__;

// Only used in www builds.
export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
export const enableSelectiveHydration = false;

// Only used in www builds.
export const enableSchedulerDebugging = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Expand Up @@ -22,6 +22,7 @@ export const enableUserTimingAPI = __DEV__;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableStableConcurrentModeAPIs = false;
export const warnAboutShorthandPropertyCollision = false;
export const enableSchedulerDebugging = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Expand Up @@ -20,6 +20,7 @@ export const warnAboutDeprecatedLifecycles = true;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableStableConcurrentModeAPIs = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.persistent.js
Expand Up @@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableStableConcurrentModeAPIs = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Expand Up @@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableStableConcurrentModeAPIs = false;
Expand Down
Expand Up @@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;
export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableStableConcurrentModeAPIs = false;
export const enableSchedulerDebugging = false;
export const warnAboutDeprecatedSetNativeProps = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.www.js
Expand Up @@ -16,6 +16,7 @@ export const {
debugRenderPhaseSideEffectsForStrictMode,
disableInputAttributeSyncing,
enableTrustedTypesIntegration,
enableSelectiveHydration,
} = require('ReactFeatureFlags');

// In www, we have experimental support for gathering data
Expand Down

0 comments on commit 3694a3b

Please sign in to comment.