diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
new file mode 100644
index 0000000000000..06fa2de298863
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
@@ -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 (
+ {
+ e.preventDefault();
+ Scheduler.unstable_yieldValue('Clicked ' + text);
+ }}>
+ {text}
+
+ );
+ }
+
+ function App() {
+ Scheduler.unstable_yieldValue('App');
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ let finalHTML = ReactDOMServer.renderToString();
+
+ 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();
+
+ // 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);
+ });
+});
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 938cf98ca6b0a..7a7b4de04a5cb 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -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';
@@ -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,
@@ -83,6 +85,8 @@ import {
} from '../shared/HTMLNodeType';
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
+setAttemptSynchronousHydration(attemptSynchronousHydration);
+
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
let topLevelUpdateWarnings;
diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js
index 3fd1655d7b3f6..c0af2a5e2d1ba 100644
--- a/packages/react-dom/src/client/ReactDOMComponentTree.js
+++ b/packages/react-dom/src/client/ReactDOMComponentTree.js
@@ -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';
@@ -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;
diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js
index 682b9ac19b1a6..76ce3c7158232 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -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,
@@ -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 & {
@@ -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;
+ }
+ }
+ }
}
}
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 03378ee54a234..4aa0219ca9bc1 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -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';
@@ -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};
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index c7789e65fa0e2..cbcd246f0eef1 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 12e700b0dc46b..bd94a207f7045 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index cd7a5d91b13d5..bd5888cb8ba20 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js
index 0fb0154932e20..9605cccf29b98 100644
--- a/packages/shared/forks/ReactFeatureFlags.persistent.js
+++ b/packages/shared/forks/ReactFeatureFlags.persistent.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index c30a8a2081be8..bffe1419d9787 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 61144269cbca8..d376426f2b573 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -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;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index aea7c61cd8231..744e2f4514dbc 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -16,6 +16,7 @@ export const {
debugRenderPhaseSideEffectsForStrictMode,
disableInputAttributeSyncing,
enableTrustedTypesIntegration,
+ enableSelectiveHydration,
} = require('ReactFeatureFlags');
// In www, we have experimental support for gathering data