@samitouri / QOS-React-2 / commits / 3694a3b5e9

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