Selective Hydration (#16880)
* Add Feature Flag for Selective Hydration * Enable Synchronous Hydration of Discrete Events * Resolve cyclic dependency
Sebastian Markbåge committed
Sep 25, 2019 at 15:26 UTC
3694a3b5e9a5a3c80d47f9889a17d8337573d513
12 files changed
+202
-16
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
new
+117
@@ -0,0 +1,117 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+let ReactDOMServer;
15
+let Scheduler;
16
+let ReactFeatureFlags;
17
+let Suspense;
18
+
19
+function dispatchClickEvent(target) {
20
+ const mouseOutEvent = document.createEvent('MouseEvents');
21
+ mouseOutEvent.initMouseEvent(
22
+ 'click',
23
+ true,
24
+ true,
25
+ window,
26
+ 0,
27
+ 50,
28
+ 50,
29
+ 50,
30
+ 50,
31
+ false,
32
+ false,
33
+ false,
34
+ false,
35
+ 0,
36
+ target,
37
+ );
38
+ return target.dispatchEvent(mouseOutEvent);
39
+}
40
+
41
+describe('ReactDOMServerSelectiveHydration', () => {
42
+ beforeEach(() => {
43
+ jest.resetModuleRegistry();
44
+
45
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
46
+ ReactFeatureFlags.enableSuspenseServerRenderer = true;
47
+ ReactFeatureFlags.enableSelectiveHydration = true;
48
+
49
+ React = require('react');
50
+ ReactDOM = require('react-dom');
51
+ ReactDOMServer = require('react-dom/server');
52
+ Scheduler = require('scheduler');
53
+ Suspense = React.Suspense;
54
+ });
55
+
56
+ it('hydrates the target boundary synchronously during a click', async () => {
57
+ function Child({text}) {
58
+ Scheduler.unstable_yieldValue(text);
59
+ return (
60
+ <span
61
+ onClick={e => {
62
+ e.preventDefault();
63
+ Scheduler.unstable_yieldValue('Clicked ' + text);
64
+ }}>
65
+ {text}
66
+ </span>
67
+ );
68
+ }
69
+
70
+ function App() {
71
+ Scheduler.unstable_yieldValue('App');
72
+ return (
73
+ <div>
74
+ <Suspense fallback="Loading...">
75
+ <Child text="A" />
76
+ </Suspense>
77
+ <Suspense fallback="Loading...">
78
+ <Child text="B" />
79
+ </Suspense>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ let finalHTML = ReactDOMServer.renderToString(<App />);
85
+
86
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
87
+
88
+ let container = document.createElement('div');
89
+ // We need this to be in the document since we'll dispatch events on it.
90
+ document.body.appendChild(container);
91
+
92
+ container.innerHTML = finalHTML;
93
+
94
+ let span = container.getElementsByTagName('span')[1];
95
+
96
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
97
+ root.render(<App />);
98
+
99
+ // Nothing has been hydrated so far.
100
+ expect(Scheduler).toHaveYielded([]);
101
+
102
+ // This should synchronously hydrate the root App and the second suspense
103
+ // boundary.
104
+ let result = dispatchClickEvent(span);
105
+
106
+ // The event should have been canceled because we called preventDefault.
107
+ expect(result).toBe(false);
108
+
109
+ // We rendered App, B and then invoked the event without rendering A.
110
+ expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
111
+
112
+ // After continuing the scheduler, we finally hydrate A.
113
+ expect(Scheduler).toFlushAndYield(['A']);
114
+
115
+ document.body.removeChild(container);
116
+ });
117
+});
packages/react-dom/src/client/ReactDOM.js
+4
@@ -39,6 +39,7 @@ import {
39
findHostInstanceWithWarning,
40
flushPassiveEffects,
41
IsThisRendererActing,
42
+ attemptSynchronousHydration,
43
} from 'react-reconciler/inline.dom';
44
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
45
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -74,6 +75,7 @@ import {
75
} from './ReactDOMComponentTree';
76
import {restoreControlledState} from './ReactDOMComponent';
77
import {dispatchEvent} from '../events/ReactDOMEventListener';
78
+import {setAttemptSynchronousHydration} from '../events/ReactDOMEventReplaying';
79
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
80
import {
81
ELEMENT_NODE,
@@ -83,6 +85,8 @@ import {
85
} from '../shared/HTMLNodeType';
86
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
87
88
+setAttemptSynchronousHydration(attemptSynchronousHydration);
89
+
90
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
91
92
let topLevelUpdateWarnings;
packages/react-dom/src/client/ReactDOMComponentTree.js
+13
-3
@@ -5,7 +5,12 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {HostComponent, HostText} from 'shared/ReactWorkTags';
8
+import {
9
+ HostComponent,
10
+ HostText,
11
+ HostRoot,
12
+ SuspenseComponent,
13
+} from 'shared/ReactWorkTags';
14
import invariant from 'shared/invariant';
15
16
import {getParentSuspenseInstance} from './ReactDOMHostConfig';
@@ -112,9 +117,14 @@ export function getClosestInstanceFromNode(targetNode) {
117
* instance, or null if the node was not rendered by this React.
118
*/
119
export function getInstanceFromNode(node) {
115
- const inst = node[internalInstanceKey];
120
+ const inst = node[internalInstanceKey] || node[internalContainerInstanceKey];
121
if (inst) {
117
- if (inst.tag === HostComponent || inst.tag === HostText) {
122
+ if (
123
+ inst.tag === HostComponent ||
124
+ inst.tag === HostText ||
125
+ inst.tag === SuspenseComponent ||
126
+ inst.tag === HostRoot
127
+ ) {
128
return inst;
129
} else {
130
return null;
packages/react-dom/src/events/ReactDOMEventReplaying.js
+40
-12
@@ -12,7 +12,10 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
12
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
13
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
14
15
-import {enableFlareAPI} from 'shared/ReactFeatureFlags';
15
+import {
16
+ enableFlareAPI,
17
+ enableSelectiveHydration,
18
+} from 'shared/ReactFeatureFlags';
19
import {
20
unstable_scheduleCallback as scheduleCallback,
21
unstable_NormalPriority as NormalPriority,
@@ -25,8 +28,15 @@ import {
28
getListeningSetForElement,
29
listenToTopLevel,
30
} from './ReactBrowserEventEmitter';
31
+import {getInstanceFromNode} from '../client/ReactDOMComponentTree';
32
import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';
33
34
+let attemptSynchronousHydration: (fiber: Object) => void;
35
+
36
+export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) {
37
+ attemptSynchronousHydration = fn;
38
+}
39
+
40
// TODO: Upgrade this definition once we're on a newer version of Flow that
41
// has this definition built-in.
42
type PointerEvent = Event & {
@@ -223,18 +233,36 @@ export function queueDiscreteEvent(
233
eventSystemFlags: EventSystemFlags,
234
nativeEvent: AnyNativeEvent,
235
): void {
226
- queuedDiscreteEvents.push(
227
- createQueuedReplayableEvent(
228
- blockedOn,
229
- topLevelType,
230
- eventSystemFlags,
231
- nativeEvent,
232
- ),
236
+ const queuedEvent = createQueuedReplayableEvent(
237
+ blockedOn,
238
+ topLevelType,
239
+ eventSystemFlags,
240
+ nativeEvent,
241
);
234
- if (blockedOn === null && queuedDiscreteEvents.length === 1) {
235
- // This probably shouldn't happen but some defensive coding might
236
- // help us get unblocked if we have a bug.
237
- replayUnblockedEvents();
242
+ queuedDiscreteEvents.push(queuedEvent);
243
+ if (enableSelectiveHydration) {
244
+ if (queuedDiscreteEvents.length === 1) {
245
+ // If this was the first discrete event, we might be able to
246
+ // synchronously unblock it so that preventDefault still works.
247
+ while (queuedEvent.blockedOn !== null) {
248
+ let fiber = getInstanceFromNode(queuedEvent.blockedOn);
249
+ if (fiber === null) {
250
+ break;
251
+ }
252
+ attemptSynchronousHydration(fiber);
253
+ if (queuedEvent.blockedOn === null) {
254
+ // We got unblocked by hydration. Let's try again.
255
+ replayUnblockedEvents();
256
+ // If we're reblocked, on an inner boundary, we might need
257
+ // to attempt hydrating that one.
258
+ continue;
259
+ } else {
260
+ // We're still blocked from hydation, we have to give up
261
+ // and replay later.
262
+ break;
263
+ }
264
+ }
265
+ }
266
}
267
}
268
packages/react-reconciler/src/ReactFiberReconciler.js
+21
-1
@@ -27,7 +27,12 @@ import {
27
findCurrentHostFiberWithNoPortals,
28
} from 'react-reconciler/reflection';
29
import {get as getInstance} from 'shared/ReactInstanceMap';
30
-import {HostComponent, ClassComponent} from 'shared/ReactWorkTags';
30
+import {
31
+ HostComponent,
32
+ ClassComponent,
33
+ HostRoot,
34
+ SuspenseComponent,
35
+} from 'shared/ReactWorkTags';
36
import getComponentName from 'shared/getComponentName';
37
import invariant from 'shared/invariant';
38
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -362,6 +367,21 @@ export function getPublicRootInstance(
367
}
368
}
369
370
+export function attemptSynchronousHydration(fiber: Fiber): void {
371
+ switch (fiber.tag) {
372
+ case HostRoot:
373
+ let root: FiberRoot = fiber.stateNode;
374
+ if (root.hydrate) {
375
+ // Flush the first scheduled "update".
376
+ flushRoot(root, root.firstPendingTime);
377
+ }
378
+ break;
379
+ case SuspenseComponent:
380
+ flushSync(() => scheduleWork(fiber, Sync));
381
+ break;
382
+ }
383
+}
384
+
385
export {findHostInstance};
386
387
export {findHostInstanceWithWarning};
packages/shared/ReactFeatureFlags.js
+1
@@ -33,6 +33,7 @@ export const enableSchedulerTracing = __PROFILE__;
33
34
// Only used in www builds.
35
export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
36
+export const enableSelectiveHydration = false;
37
38
// Only used in www builds.
39
export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -22,6 +22,7 @@ export const enableUserTimingAPI = __DEV__;
22
export const enableProfilerTimer = __PROFILE__;
23
export const enableSchedulerTracing = __PROFILE__;
24
export const enableSuspenseServerRenderer = false;
25
+export const enableSelectiveHydration = false;
26
export const enableStableConcurrentModeAPIs = false;
27
export const warnAboutShorthandPropertyCollision = false;
28
export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -20,6 +20,7 @@ export const warnAboutDeprecatedLifecycles = true;
20
export const enableProfilerTimer = __PROFILE__;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
+export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
export const enableStableConcurrentModeAPIs = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
20
export const enableProfilerTimer = __PROFILE__;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
+export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
export const enableStableConcurrentModeAPIs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
20
export const enableProfilerTimer = __PROFILE__;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
+export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
export const enableStableConcurrentModeAPIs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
20
export const enableProfilerTimer = __PROFILE__;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
+export const enableSelectiveHydration = false;
24
export const enableStableConcurrentModeAPIs = false;
25
export const enableSchedulerDebugging = false;
26
export const warnAboutDeprecatedSetNativeProps = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -16,6 +16,7 @@ export const {
16
debugRenderPhaseSideEffectsForStrictMode,
17
disableInputAttributeSyncing,
18
enableTrustedTypesIntegration,
19
+ enableSelectiveHydration,
20
} = require('ReactFeatureFlags');
21
22
// In www, we have experimental support for gathering data