Skip to content

Commit 9c10ac4

Browse files
committed
[ServerRenderer] Add option to send instructions as data attributes (facebook#25437)
### Changes made: - Running with enableFizzExternalRuntime (feature flag) and unstable_externalRuntimeSrc (param) will generate html nodes with data attributes that encode Fizz instructions. ``` <div hidden data-rxi="" data-bid="param0" data-dgst="param1" ></div> ``` - Added an external runtime browser script `ReactDOMServerExternalRuntime`, which processes and removes these nodes - This runtime should be passed as to renderInto[...] via `unstable_externalRuntimeSrc` - Since this runtime is render blocking (for all streamed suspense boundaries and segments), we want this to reach the client as early as possible. By default, Fizz will send this script at the end of the shell when it detects dynamic content (e.g. suspenseful pending tasks), but it can be sent even earlier by calling `preinit(...)` inside a component. - The current implementation relies on Float to dedupe sending `unstable_externalRuntimeSrc`, so `enableFizzExternalRuntime` is only valid when `enableFloat` is also set.
1 parent bf33d4f commit 9c10ac4

File tree

12 files changed

+845
-151
lines changed

12 files changed

+845
-151
lines changed

packages/react-dom-bindings/src/server/ReactDOMFloatServer.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ type PreinitOptions = {
275275
crossOrigin?: string,
276276
integrity?: string,
277277
};
278-
function preinit(href: string, options: PreinitOptions) {
278+
function preinit(href: string, options: PreinitOptions): void {
279279
if (!currentResources) {
280280
// While we expect that preinit calls are primarily going to be observed
281281
// during render because effects and events don't run on the server it is
@@ -285,7 +285,17 @@ function preinit(href: string, options: PreinitOptions) {
285285
// simply return and do not warn.
286286
return;
287287
}
288-
const resources = currentResources;
288+
preinitImpl(currentResources, href, options);
289+
}
290+
291+
// On the server, preinit may be called outside of render when sending an
292+
// external SSR runtime as part of the initial resources payload. Since this
293+
// is an internal React call, we do not need to use the resources stack.
294+
export function preinitImpl(
295+
resources: Resources,
296+
href: string,
297+
options: PreinitOptions,
298+
): void {
289299
if (__DEV__) {
290300
validatePreinitArguments(href, options);
291301
}

packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js

+92-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* clients. Therefore, it should be fast and not have many external dependencies.
44
* @flow
55
*/
6+
/* eslint-disable dot-notation */
67

78
// Imports are resolved statically by the closure compiler in release bundles
89
// and by rollup in jest unit tests
@@ -13,13 +14,94 @@ import {
1314
completeSegment,
1415
} from './fizz-instruction-set/ReactDOMFizzInstructionSet';
1516

16-
// Intentionally does nothing. Implementation will be added in future PR.
17-
// eslint-disable-next-line no-unused-vars
18-
const observer = new MutationObserver(mutations => {
19-
// These are only called so I can check what the module output looks like. The
20-
// code is unreachable.
21-
clientRenderBoundary();
22-
completeBoundaryWithStyles();
23-
completeBoundary();
24-
completeSegment();
25-
});
17+
if (!window.$RC) {
18+
// TODO: Eventually remove, we currently need to set these globals for
19+
// compatibility with ReactDOMFizzInstructionSet
20+
window.$RC = completeBoundary;
21+
window.$RM = new Map();
22+
}
23+
24+
if (document.readyState === 'loading') {
25+
if (document.body != null) {
26+
installFizzInstrObserver(document.body);
27+
} else {
28+
// body may not exist yet if the fizz runtime is sent in <head>
29+
// (e.g. as a preinit resource)
30+
const domBodyObserver = new MutationObserver(() => {
31+
// We expect the body node to be stable once parsed / created
32+
if (document.body) {
33+
if (document.readyState === 'loading') {
34+
installFizzInstrObserver(document.body);
35+
}
36+
handleExistingNodes();
37+
domBodyObserver.disconnect();
38+
}
39+
});
40+
// documentElement must already exist at this point
41+
// $FlowFixMe[incompatible-call]
42+
domBodyObserver.observe(document.documentElement, {childList: true});
43+
}
44+
}
45+
46+
handleExistingNodes();
47+
48+
function handleExistingNodes() {
49+
const existingNodes = document.getElementsByTagName('template');
50+
for (let i = 0; i < existingNodes.length; i++) {
51+
handleNode(existingNodes[i]);
52+
}
53+
}
54+
55+
function installFizzInstrObserver(target /*: Node */) {
56+
const fizzInstrObserver = new MutationObserver(mutations => {
57+
for (let i = 0; i < mutations.length; i++) {
58+
const addedNodes = mutations[i].addedNodes;
59+
for (let j = 0; j < addedNodes.length; j++) {
60+
if (addedNodes.item(j).parentNode) {
61+
handleNode(addedNodes.item(j));
62+
}
63+
}
64+
}
65+
});
66+
// We assume that instruction data nodes are eventually appended to the
67+
// body, even if Fizz is streaming to a shell / subtree.
68+
fizzInstrObserver.observe(target, {
69+
childList: true,
70+
});
71+
window.addEventListener('DOMContentLoaded', () => {
72+
fizzInstrObserver.disconnect();
73+
});
74+
}
75+
76+
function handleNode(node_ /*: Node */) {
77+
// $FlowFixMe[incompatible-cast]
78+
if (node_.nodeType !== 1 || !(node_ /*: HTMLElement*/).dataset) {
79+
return;
80+
}
81+
// $FlowFixMe[incompatible-cast]
82+
const node = (node_ /*: HTMLElement*/);
83+
const dataset = node.dataset;
84+
if (dataset['rxi'] != null) {
85+
clientRenderBoundary(
86+
dataset['bid'],
87+
dataset['dgst'],
88+
dataset['msg'],
89+
dataset['stck'],
90+
);
91+
node.remove();
92+
} else if (dataset['rri'] != null) {
93+
// Convert styles here, since its type is Array<Array<string>>
94+
completeBoundaryWithStyles(
95+
dataset['bid'],
96+
dataset['sid'],
97+
JSON.parse(dataset['sty']),
98+
);
99+
node.remove();
100+
} else if (dataset['rci'] != null) {
101+
completeBoundary(dataset['bid'], dataset['sid']);
102+
node.remove();
103+
} else if (dataset['rsi'] != null) {
104+
completeSegment(dataset['sid'], dataset['pid']);
105+
node.remove();
106+
}
107+
}

0 commit comments

Comments
 (0)