Skip to content

Commit

Permalink
Enable Synchronous Hydration of Discrete Events
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Sep 25, 2019
1 parent 3d656d3 commit d934ed3
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});
16 changes: 13 additions & 3 deletions packages/react-dom/src/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
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
47 changes: 35 additions & 12 deletions packages/react-dom/src/events/ReactDOMEventReplaying.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ 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,
} from 'scheduler';
import {attemptSynchronousHydration} from 'react-reconciler/inline.dom';
import {
attemptToDispatchEvent,
trapEventForResponderEventSystem,
Expand All @@ -25,6 +29,7 @@ import {
getListeningSetForElement,
listenToTopLevel,
} from './ReactBrowserEventEmitter';
import {getInstanceFromNode} from '../client/ReactDOMComponentTree';
import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';

// TODO: Upgrade this definition once we're on a newer version of Flow that
Expand Down Expand Up @@ -223,18 +228,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
Original file line number Diff line number Diff line change
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

0 comments on commit d934ed3

Please sign in to comment.