@samitouri / QOS-React-2 / commits / d47339ea36

[Fizz] Remove assignID mechanism (#22410)

* Remove pushEmpty This is only used to support the assignID mechanism. * Remove assignID mechanism This effectively isn't used anyway because we always insert a dummy tag into the fallback. * Emit the template tag with an ID directly in pending boundaries This ensures that assigning the ID is deterministic since it's done during writing. This also avoids emitting it for client rendered boundaries that start as client rendered since we never need to refer to them. * Move lazy ID initialization to the core implementation We never need an ID before we write a pending boundary. This also ensures that ID generation is deterministic by moving it to the write phase. * Simplify the inserted scripts We can assume that there are no text nodes before the template tag so this simplifies the script that finds the comment node. It should be the direct previous child.

Sebastian Markbåge committed Sep 24, 2021 at 10:22 UTC d47339ea3659133940f7699144cd30453fb6ed43
6 files changed +73 -278
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+45 -185
@@ -168,17 +168,17 @@ export function getChildFormatContext(
168 return parentContext;
169 }
170
171 -// This object is used to lazily reuse the ID of the first generated node, or assign one.
172 -// We can't assign an ID up front because the node we're attaching it to might already
173 -// have one. So we need to lazily use that if it's available.
174 -export type SuspenseBoundaryID = {
175 - formattedID: null | PrecomputedChunk,
176 -};
171 +export type SuspenseBoundaryID = null | PrecomputedChunk;
172 +
173 +export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID: SuspenseBoundaryID = null;
174
178 -export function createSuspenseBoundaryID(
175 +export function assignSuspenseBoundaryID(
176 responseState: ResponseState,
177 ): SuspenseBoundaryID {
181 - return {formattedID: null};
178 + const generatedID = responseState.nextSuspenseID++;
179 + return stringToPrecomputedChunk(
180 + responseState.boundaryPrefix + generatedID.toString(16),
181 + );
182 }
183
184 export type OpaqueIDType = string;
@@ -201,50 +201,13 @@ function encodeHTMLTextNode(text: string): string {
201 return escapeTextForBrowser(text);
202 }
203
204 -function assignAnID(
205 - responseState: ResponseState,
206 - id: SuspenseBoundaryID,
207 -): PrecomputedChunk {
208 - // TODO: This approach doesn't yield deterministic results since this is assigned during render.
209 - const generatedID = responseState.nextSuspenseID++;
210 - return (id.formattedID = stringToPrecomputedChunk(
211 - responseState.boundaryPrefix + generatedID.toString(16),
212 - ));
213 -}
214 -
215 -const dummyNode1 = stringToPrecomputedChunk('<template id="');
216 -const dummyNode2 = stringToPrecomputedChunk('"></template>');
217 -
218 -function pushDummyNodeWithID(
219 - target: Array<Chunk | PrecomputedChunk>,
220 - responseState: ResponseState,
221 - assignID: SuspenseBoundaryID,
222 -): void {
223 - const id = assignAnID(responseState, assignID);
224 - target.push(dummyNode1, id, dummyNode2);
225 -}
226 -
227 -export function pushEmpty(
228 - target: Array<Chunk | PrecomputedChunk>,
229 - responseState: ResponseState,
230 - assignID: null | SuspenseBoundaryID,
231 -): void {
232 - if (assignID !== null) {
233 - pushDummyNodeWithID(target, responseState, assignID);
234 - }
235 -}
236 -
204 const textSeparator = stringToPrecomputedChunk('<!-- -->');
205
206 export function pushTextInstance(
207 target: Array<Chunk | PrecomputedChunk>,
208 text: string,
209 responseState: ResponseState,
243 - assignID: null | SuspenseBoundaryID,
210 ): void {
245 - if (assignID !== null) {
246 - pushDummyNodeWithID(target, responseState, assignID);
247 - }
211 if (text === '') {
212 // Empty text doesn't have a DOM node representation and the hydration is aware of this.
213 return;
@@ -514,30 +477,6 @@ function pushAttribute(
477 const endOfStartTag = stringToPrecomputedChunk('>');
478 const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');
479
517 -const idAttr = stringToPrecomputedChunk(' id="');
518 -const attrEnd = stringToPrecomputedChunk('"');
519 -
520 -function pushID(
521 - target: Array<Chunk | PrecomputedChunk>,
522 - responseState: ResponseState,
523 - assignID: SuspenseBoundaryID,
524 - existingID: mixed,
525 -): void {
526 - if (
527 - existingID !== null &&
528 - existingID !== undefined &&
529 - (typeof existingID === 'string' || typeof existingID === 'object')
530 - ) {
531 - // We can reuse the existing ID for our purposes.
532 - assignID.formattedID = stringToPrecomputedChunk(
533 - escapeTextForBrowser(existingID),
534 - );
535 - } else {
536 - const encodedID = assignAnID(responseState, assignID);
537 - target.push(idAttr, encodedID, attrEnd);
538 - }
539 -}
540 -
480 function pushInnerHTML(
481 target: Array<Chunk | PrecomputedChunk>,
482 innerHTML,
@@ -598,7 +537,6 @@ function pushStartSelect(
537 target: Array<Chunk | PrecomputedChunk>,
538 props: Object,
539 responseState: ResponseState,
601 - assignID: null | SuspenseBoundaryID,
540 ): ReactNodeList {
541 if (__DEV__) {
542 checkControlledValueProps('select', props);
@@ -651,9 +589,6 @@ function pushStartSelect(
589 }
590 }
591 }
654 - if (assignID !== null) {
655 - pushID(target, responseState, assignID, props.id);
656 - }
592
593 target.push(endOfStartTag);
594 pushInnerHTML(target, innerHTML, children);
@@ -693,7 +628,6 @@ function pushStartOption(
628 props: Object,
629 responseState: ResponseState,
630 formatContext: FormatContext,
696 - assignID: null | SuspenseBoundaryID,
631 ): ReactNodeList {
632 const selectedValue = formatContext.selectedValue;
633
@@ -776,10 +710,6 @@ function pushStartOption(
710 target.push(selectedMarkerAttribute);
711 }
712
779 - if (assignID !== null) {
780 - pushID(target, responseState, assignID, props.id);
781 - }
782 -
713 target.push(endOfStartTag);
714 pushInnerHTML(target, innerHTML, children);
715 return children;
@@ -789,7 +719,6 @@ function pushInput(
719 target: Array<Chunk | PrecomputedChunk>,
720 props: Object,
721 responseState: ResponseState,
792 - assignID: null | SuspenseBoundaryID,
722 ): ReactNodeList {
723 if (__DEV__) {
724 checkControlledValueProps('input', props);
@@ -883,10 +812,6 @@ function pushInput(
812 pushAttribute(target, responseState, 'value', defaultValue);
813 }
814
886 - if (assignID !== null) {
887 - pushID(target, responseState, assignID, props.id);
888 - }
889 -
815 target.push(endOfStartTagSelfClosing);
816 return null;
817 }
@@ -895,7 +820,6 @@ function pushStartTextArea(
820 target: Array<Chunk | PrecomputedChunk>,
821 props: Object,
822 responseState: ResponseState,
898 - assignID: null | SuspenseBoundaryID,
823 ): ReactNodeList {
824 if (__DEV__) {
825 checkControlledValueProps('textarea', props);
@@ -952,10 +876,6 @@ function pushStartTextArea(
876 value = defaultValue;
877 }
878
955 - if (assignID !== null) {
956 - pushID(target, responseState, assignID, props.id);
957 - }
958 -
879 target.push(endOfStartTag);
880
881 // TODO (yungsters): Remove support for children content in <textarea>.
@@ -1002,7 +922,6 @@ function pushSelfClosing(
922 props: Object,
923 tag: string,
924 responseState: ResponseState,
1005 - assignID: null | SuspenseBoundaryID,
925 ): ReactNodeList {
926 target.push(startChunkForTag(tag));
927
@@ -1028,9 +947,6 @@ function pushSelfClosing(
947 }
948 }
949 }
1031 - if (assignID !== null) {
1032 - pushID(target, responseState, assignID, props.id);
1033 - }
950
951 target.push(endOfStartTagSelfClosing);
952 return null;
@@ -1040,7 +956,6 @@ function pushStartMenuItem(
956 target: Array<Chunk | PrecomputedChunk>,
957 props: Object,
958 responseState: ResponseState,
1043 - assignID: null | SuspenseBoundaryID,
959 ): ReactNodeList {
960 target.push(startChunkForTag('menuitem'));
961
@@ -1064,9 +979,6 @@ function pushStartMenuItem(
979 }
980 }
981 }
1067 - if (assignID !== null) {
1068 - pushID(target, responseState, assignID, props.id);
1069 - }
982
983 target.push(endOfStartTag);
984 return null;
@@ -1077,7 +989,6 @@ function pushStartGenericElement(
989 props: Object,
990 tag: string,
991 responseState: ResponseState,
1080 - assignID: null | SuspenseBoundaryID,
992 ): ReactNodeList {
993 target.push(startChunkForTag(tag));
994
@@ -1102,9 +1013,6 @@ function pushStartGenericElement(
1013 }
1014 }
1015 }
1105 - if (assignID !== null) {
1106 - pushID(target, responseState, assignID, props.id);
1107 - }
1016
1017 target.push(endOfStartTag);
1018 pushInnerHTML(target, innerHTML, children);
@@ -1122,7 +1030,6 @@ function pushStartCustomElement(
1030 props: Object,
1031 tag: string,
1032 responseState: ResponseState,
1125 - assignID: null | SuspenseBoundaryID,
1033 ): ReactNodeList {
1034 target.push(startChunkForTag(tag));
1035
@@ -1166,9 +1073,6 @@ function pushStartCustomElement(
1073 }
1074 }
1075 }
1169 - if (assignID !== null) {
1170 - pushID(target, responseState, assignID, props.id);
1171 - }
1076
1077 target.push(endOfStartTag);
1078 pushInnerHTML(target, innerHTML, children);
@@ -1182,7 +1086,6 @@ function pushStartPreformattedElement(
1086 props: Object,
1087 tag: string,
1088 responseState: ResponseState,
1185 - assignID: null | SuspenseBoundaryID,
1089 ): ReactNodeList {
1090 target.push(startChunkForTag(tag));
1091
@@ -1207,9 +1110,6 @@ function pushStartPreformattedElement(
1110 }
1111 }
1112 }
1210 - if (assignID !== null) {
1211 - pushID(target, responseState, assignID, props.id);
1212 - }
1113
1114 target.push(endOfStartTag);
1115
@@ -1275,7 +1175,6 @@ export function pushStartInstance(
1175 props: Object,
1176 responseState: ResponseState,
1177 formatContext: FormatContext,
1278 - assignID: null | SuspenseBoundaryID,
1178 ): ReactNodeList {
1179 if (__DEV__) {
1180 validateARIAProperties(type, props);
@@ -1317,31 +1216,19 @@ export function pushStartInstance(
1216 switch (type) {
1217 // Special tags
1218 case 'select':
1320 - return pushStartSelect(target, props, responseState, assignID);
1219 + return pushStartSelect(target, props, responseState);
1220 case 'option':
1322 - return pushStartOption(
1323 - target,
1324 - props,
1325 - responseState,
1326 - formatContext,
1327 - assignID,
1328 - );
1221 + return pushStartOption(target, props, responseState, formatContext);
1222 case 'textarea':
1330 - return pushStartTextArea(target, props, responseState, assignID);
1223 + return pushStartTextArea(target, props, responseState);
1224 case 'input':
1332 - return pushInput(target, props, responseState, assignID);
1225 + return pushInput(target, props, responseState);
1226 case 'menuitem':
1334 - return pushStartMenuItem(target, props, responseState, assignID);
1227 + return pushStartMenuItem(target, props, responseState);
1228 // Newline eating tags
1229 case 'listing':
1230 case 'pre': {
1338 - return pushStartPreformattedElement(
1339 - target,
1340 - props,
1341 - type,
1342 - responseState,
1343 - assignID,
1344 - );
1231 + return pushStartPreformattedElement(target, props, type, responseState);
1232 }
1233 // Omitted close tags
1234 case 'area':
@@ -1358,7 +1245,7 @@ export function pushStartInstance(
1245 case 'source':
1246 case 'track':
1247 case 'wbr': {
1361 - return pushSelfClosing(target, props, type, responseState, assignID);
1248 + return pushSelfClosing(target, props, type, responseState);
1249 }
1250 // These are reserved SVG and MathML elements, that are never custom elements.
1251 // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
@@ -1370,13 +1257,7 @@ export function pushStartInstance(
1257 case 'font-face-format':
1258 case 'font-face-name':
1259 case 'missing-glyph': {
1373 - return pushStartGenericElement(
1374 - target,
1375 - props,
1376 - type,
1377 - responseState,
1378 - assignID,
1379 - );
1260 + return pushStartGenericElement(target, props, type, responseState);
1261 }
1262 case 'html': {
1263 if (formatContext.insertionMode === ROOT_HTML_MODE) {
@@ -1385,33 +1266,15 @@ export function pushStartInstance(
1266 // rendering the whole document.
1267 target.push(DOCTYPE);
1268 }
1388 - return pushStartGenericElement(
1389 - target,
1390 - props,
1391 - type,
1392 - responseState,
1393 - assignID,
1394 - );
1269 + return pushStartGenericElement(target, props, type, responseState);
1270 }
1271 default: {
1272 if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
1273 // Generic element
1399 - return pushStartGenericElement(
1400 - target,
1401 - props,
1402 - type,
1403 - responseState,
1404 - assignID,
1405 - );
1274 + return pushStartGenericElement(target, props, type, responseState);
1275 } else {
1276 // Custom element
1408 - return pushStartCustomElement(
1409 - target,
1410 - props,
1411 - type,
1412 - responseState,
1413 - assignID,
1414 - );
1277 + return pushStartCustomElement(target, props, type, responseState);
1278 }
1279 }
1280 }
@@ -1474,7 +1337,10 @@ export function writePlaceholder(
1337
1338 // Suspense boundaries are encoded as comments.
1339 const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
1477 -const startPendingSuspenseBoundary = stringToPrecomputedChunk('<!--$?-->');
1340 +const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
1341 + '<!--$?--><template id="',
1342 +);
1343 +const startPendingSuspenseBoundary2 = stringToPrecomputedChunk('"></template>');
1344 const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
1345 '<!--$!-->',
1346 );
@@ -1503,7 +1369,13 @@ export function writeStartPendingSuspenseBoundary(
1369 responseState: ResponseState,
1370 id: SuspenseBoundaryID,
1371 ): boolean {
1506 - return writeChunk(destination, startPendingSuspenseBoundary);
1372 + writeChunk(destination, startPendingSuspenseBoundary1);
1373 + invariant(
1374 + id !== null,
1375 + 'An ID must have been assigned before we can complete the boundary.',
1376 + );
1377 + writeChunk(destination, id);
1378 + return writeChunk(destination, startPendingSuspenseBoundary2);
1379 }
1380 export function writeStartClientRenderedSuspenseBoundary(
1381 destination: Destination,
@@ -1671,19 +1543,14 @@ export function writeEndSegment(
1543 //
1544 // function clientRenderBoundary(suspenseBoundaryID) {
1545 // // Find the fallback's first element.
1674 -// let suspenseNode = document.getElementById(suspenseBoundaryID);
1675 -// if (!suspenseNode) {
1546 +// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
1547 +// if (!suspenseIdNode) {
1548 // // The user must have already navigated away from this tree.
1549 // // E.g. because the parent was hydrated.
1550 // return;
1551 // }
1680 -// // Find the boundary around the fallback. This might include text nodes.
1681 -// do {
1682 -// suspenseNode = suspenseNode.previousSibling;
1683 -// } while (
1684 -// suspenseNode.nodeType !== COMMENT_NODE ||
1685 -// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
1686 -// );
1552 +// // Find the boundary around the fallback. This is always the previous node.
1553 +// const suspenseNode = suspenseIdNode.previousSibling;
1554 // // Tag it to be client rendered.
1555 // suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
1556 // // Tell React to retry it if the parent already hydrated.
@@ -1694,24 +1561,19 @@ export function writeEndSegment(
1561 //
1562 // function completeBoundary(suspenseBoundaryID, contentID) {
1563 // // Find the fallback's first element.
1697 -// let suspenseNode = document.getElementById(suspenseBoundaryID);
1564 +// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
1565 // const contentNode = document.getElementById(contentID);
1566 // // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
1567 // // This might also help by not causing recalcing each time we move a child from here to the target.
1568 // contentNode.parentNode.removeChild(contentNode);
1702 -// if (!suspenseNode) {
1569 +// if (!suspenseIdNode) {
1570 // // The user must have already navigated away from this tree.
1571 // // E.g. because the parent was hydrated. That's fine there's nothing to do
1572 // // but we have to make sure that we already deleted the container node.
1573 // return;
1574 // }
1708 -// // Find the boundary around the fallback. This might include text nodes.
1709 -// do {
1710 -// suspenseNode = suspenseNode.previousSibling;
1711 -// } while (
1712 -// suspenseNode.nodeType !== COMMENT_NODE ||
1713 -// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
1714 -// );
1575 +// // Find the boundary around the fallback. This is always the previous node.
1576 +// const suspenseNode = suspenseIdNode.previousSibling;
1577 //
1578 // // Clear all the existing children. This is complicated because
1579 // // there can be embedded Suspense boundaries in the fallback.
@@ -1774,11 +1636,11 @@ export function writeEndSegment(
1636 // }
1637
1638 const completeSegmentFunction =
1777 - 'function $RS(b,f){var a=document.getElementById(b),c=document.getElementById(f);for(a.parentNode.removeChild(a);a.firstChild;)c.parentNode.insertBefore(a.firstChild,c);c.parentNode.removeChild(c)}';
1639 + 'function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)}';
1640 const completeBoundaryFunction =
1779 - 'function $RC(b,f){var a=document.getElementById(b),c=document.getElementById(f);c.parentNode.removeChild(c);if(a){do a=a.previousSibling;while(8!==a.nodeType||"$?"!==a.data);var h=a.parentNode,d=a.nextSibling,g=0;do{if(d&&8===d.nodeType){var e=d.data;if("/$"===e)if(0===g)break;else g--;else"$"!==e&&"$?"!==e&&"$!"!==e||g++}e=d.nextSibling;h.removeChild(d);d=e}while(d);for(;c.firstChild;)h.insertBefore(c.firstChild,d);a.data="$";a._reactRetry&&a._reactRetry()}}';
1641 + 'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}';
1642 const clientRenderFunction =
1781 - 'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
1643 + 'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';
1644
1645 const completeSegmentScript1Full = stringToPrecomputedChunk(
1646 '<script>' + completeSegmentFunction + ';$RS("',
@@ -1832,13 +1694,12 @@ export function writeCompletedBoundaryInstruction(
1694 // Future calls can just reuse the same function.
1695 writeChunk(destination, completeBoundaryScript1Partial);
1696 }
1835 - const formattedBoundaryID = boundaryID.formattedID;
1697 invariant(
1837 - formattedBoundaryID !== null,
1698 + boundaryID !== null,
1699 'An ID must have been assigned before we can complete the boundary.',
1700 );
1701 const formattedContentID = stringToChunk(contentSegmentID.toString(16));
1841 - writeChunk(destination, formattedBoundaryID);
1702 + writeChunk(destination, boundaryID);
1703 writeChunk(destination, completeBoundaryScript2);
1704 writeChunk(destination, responseState.segmentPrefix);
1705 writeChunk(destination, formattedContentID);
@@ -1864,11 +1725,10 @@ export function writeClientRenderBoundaryInstruction(
1725 // Future calls can just reuse the same function.
1726 writeChunk(destination, clientRenderScript1Partial);
1727 }
1867 - const formattedBoundaryID = boundaryID.formattedID;
1728 invariant(
1869 - formattedBoundaryID !== null,
1729 + boundaryID !== null,
1730 'An ID must have been assigned before we can complete the boundary.',
1731 );
1872 - writeChunk(destination, formattedBoundaryID);
1732 + writeChunk(destination, boundaryID);
1733 return writeChunk(destination, clientRenderScript2);
1734 }
packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+4 -8
@@ -7,10 +7,7 @@
7 * @flow
8 */
9
10 -import type {
11 - SuspenseBoundaryID,
12 - FormatContext,
13 -} from './ReactDOMServerFormatConfig';
10 +import type {FormatContext} from './ReactDOMServerFormatConfig';
11
12 import {
13 createResponseState as createResponseStateImpl,
@@ -81,9 +78,9 @@ export type {
78
79 export {
80 getChildFormatContext,
84 - createSuspenseBoundaryID,
81 + UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
82 + assignSuspenseBoundaryID,
83 makeServerID,
86 - pushEmpty,
84 pushStartInstance,
85 pushEndInstance,
86 pushStartCompletedSuspenseBoundary,
@@ -106,12 +103,11 @@ export function pushTextInstance(
103 target: Array<Chunk | PrecomputedChunk>,
104 text: string,
105 responseState: ResponseState,
109 - assignID: null | SuspenseBoundaryID,
106 ): void {
107 if (responseState.generateStaticMarkup) {
108 target.push(stringToChunk(escapeTextForBrowser(text)));
109 } else {
114 - pushTextInstanceImpl(target, text, responseState, assignID);
110 + pushTextInstanceImpl(target, text, responseState);
111 }
112 }
113
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+3 -12
@@ -103,10 +103,11 @@ export function getChildFormatContext(
103 // This is very specific to DOM where we can't assign an ID to.
104 export type SuspenseBoundaryID = number;
105
106 -export function createSuspenseBoundaryID(
106 +export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = -1;
107 +
108 +export function assignSuspenseBoundaryID(
109 responseState: ResponseState,
110 ): SuspenseBoundaryID {
109 - // TODO: This is not deterministic since it's created during render.
111 return responseState.nextSuspenseID++;
112 }
113
@@ -125,19 +126,10 @@ export function makeServerID(
126
127 const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
128
128 -export function pushEmpty(
129 - target: Array<Chunk | PrecomputedChunk>,
130 - responseState: ResponseState,
131 - assignID: null | SuspenseBoundaryID,
132 -): void {
133 - // This is not used since we don't need to assign any IDs.
134 -}
135 -
129 export function pushTextInstance(
130 target: Array<Chunk | PrecomputedChunk>,
131 text: string,
132 responseState: ResponseState,
140 - assignID: null | SuspenseBoundaryID,
133 ): void {
134 target.push(
135 INSTANCE,
@@ -154,7 +146,6 @@ export function pushStartInstance(
146 props: Object,
147 responseState: ResponseState,
148 formatContext: FormatContext,
157 - assignID: null | SuspenseBoundaryID,
149 ): ReactNodeList {
150 target.push(
151 INSTANCE,
packages/react-noop-renderer/src/ReactNoopServer.js
+3 -1
@@ -81,7 +81,9 @@ const ReactNoopServer = ReactFizzServer({
81 closeWithError(destination: Destination, error: mixed): void {},
82 flushBuffered(destination: Destination): void {},
83
84 - createSuspenseBoundaryID(): SuspenseInstance {
84 + UNINITIALIZED_SUSPENSE_BOUNDARY_ID: null,
85 +
86 + assignSuspenseBoundaryID(): SuspenseInstance {
87 // The ID is a pointer to the boundary itself.
88 return {state: 'pending', children: []};
89 },
packages/react-server/src/ReactFizzServer.js
+15 -70
@@ -48,13 +48,13 @@ import {
48 writeClientRenderBoundaryInstruction,
49 writeCompletedBoundaryInstruction,
50 writeCompletedSegmentInstruction,
51 - pushEmpty,
51 pushTextInstance,
52 pushStartInstance,
53 pushEndInstance,
54 pushStartCompletedSuspenseBoundary,
55 pushEndCompletedSuspenseBoundary,
57 - createSuspenseBoundaryID,
56 + UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
57 + assignSuspenseBoundaryID,
58 getChildFormatContext,
59 } from './ReactServerFormatConfig';
60 import {
@@ -124,7 +124,7 @@ type LegacyContext = {
124 };
125
126 type SuspenseBoundary = {
127 - +id: SuspenseBoundaryID,
127 + id: SuspenseBoundaryID,
128 rootSegmentID: number,
129 forceClientRender: boolean, // if it errors or infinitely suspends
130 parentFlushed: boolean,
@@ -142,7 +142,6 @@ type Task = {
142 abortSet: Set<Task>, // the abortable set that this task belongs to
143 legacyContext: LegacyContext, // the current legacy context that this task is executing in
144 context: ContextSnapshot, // the current new context that this task is executing in
145 - assignID: null | SuspenseBoundaryID, // id to assign to the content
145 componentStack: null | ComponentStackNode, // DEV-only component stack
146 };
147
@@ -265,7 +264,6 @@ export function createRequest(
264 abortSet,
265 emptyContextObject,
266 rootContextSnapshot,
268 - null,
267 );
268 pingedTasks.push(rootTask);
269 return request;
@@ -284,7 +282,7 @@ function createSuspenseBoundary(
282 fallbackAbortableTasks: Set<Task>,
283 ): SuspenseBoundary {
284 return {
287 - id: createSuspenseBoundaryID(request.responseState),
285 + id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
286 rootSegmentID: -1,
287 parentFlushed: false,
288 pendingTasks: 0,
@@ -303,7 +301,6 @@ function createTask(
301 abortSet: Set<Task>,
302 legacyContext: LegacyContext,
303 context: ContextSnapshot,
306 - assignID: null | SuspenseBoundaryID,
304 ): Task {
305 request.allPendingTasks++;
306 if (blockedBoundary === null) {
@@ -319,7 +316,6 @@ function createTask(
316 abortSet,
317 legacyContext,
318 context,
322 - assignID,
319 }: any);
320 if (__DEV__) {
321 task.componentStack = null;
@@ -421,9 +417,6 @@ function renderSuspenseBoundary(
417 const parentBoundary = task.blockedBoundary;
418 const parentSegment = task.blockedSegment;
419
424 - // We need to push an "empty" thing here to identify the parent suspense boundary.
425 - pushEmpty(parentSegment.chunks, request.responseState, task.assignID);
426 - task.assignID = null;
420 // Each time we enter a suspense boundary, we split out into a new segment for
421 // the fallback so that we can later replace that segment with the content.
422 // This also lets us split out the main content even if it doesn't suspend,
@@ -488,30 +481,16 @@ function renderSuspenseBoundary(
481 task.blockedSegment = parentSegment;
482 }
483
491 - // This injects an extra segment just to contain an empty tag with an ID.
492 - // This means that we're not actually using the assignID anywhere.
493 - // TODO: Rethink the assignID approach.
494 - pushEmpty(boundarySegment.chunks, request.responseState, newBoundary.id);
495 - const innerSegment = createPendingSegment(
496 - request,
497 - boundarySegment.chunks.length,
498 - null,
499 - boundarySegment.formatContext,
500 - );
501 - boundarySegment.status = COMPLETED;
502 - boundarySegment.children.push(innerSegment);
503 -
484 // We create suspended task for the fallback because we don't want to actually work
485 // on it yet in case we finish the main content, so we queue for later.
486 const suspendedFallbackTask = createTask(
487 request,
488 fallback,
489 parentBoundary,
510 - innerSegment,
490 + boundarySegment,
491 fallbackAbortSet,
492 task.legacyContext,
493 task.context,
514 - null,
494 );
495 if (__DEV__) {
496 suspendedFallbackTask.componentStack = task.componentStack;
@@ -554,10 +533,7 @@ function renderHostElement(
533 props,
534 request.responseState,
535 segment.formatContext,
557 - task.assignID,
536 );
559 - // We must have assigned it already above so we don't need this anymore.
560 - task.assignID = null;
537 const prevContext = segment.formatContext;
538 segment.formatContext = getChildFormatContext(prevContext, type, props);
539 // We use the non-destructive form because if something suspends, we still
@@ -1143,20 +1119,11 @@ function renderNodeDestructive(
1119 }
1120
1121 if (isArray(node)) {
1146 - if (node.length > 0) {
1147 - for (let i = 0; i < node.length; i++) {
1148 - // Recursively render the rest. We need to use the non-destructive form
1149 - // so that we can safely pop back up and render the sibling if something
1150 - // suspends.
1151 - renderNode(request, task, node[i]);
1152 - }
1153 - } else {
1154 - pushEmpty(
1155 - task.blockedSegment.chunks,
1156 - request.responseState,
1157 - task.assignID,
1158 - );
1159 - task.assignID = null;
1122 + for (let i = 0; i < node.length; i++) {
1123 + // Recursively render the rest. We need to use the non-destructive form
1124 + // so that we can safely pop back up and render the sibling if something
1125 + // suspends.
1126 + renderNode(request, task, node[i]);
1127 }
1128 return;
1129 }
@@ -1181,12 +1148,6 @@ function renderNodeDestructive(
1148 return;
1149 }
1150 }
1184 - pushEmpty(
1185 - task.blockedSegment.chunks,
1186 - request.responseState,
1187 - task.assignID,
1188 - );
1189 - task.assignID = null;
1151 }
1152
1153 const childString = Object.prototype.toString.call(node);
@@ -1202,13 +1163,7 @@ function renderNodeDestructive(
1163 }
1164
1165 if (typeof node === 'string') {
1205 - pushTextInstance(
1206 - task.blockedSegment.chunks,
1207 - node,
1208 - request.responseState,
1209 - task.assignID,
1210 - );
1211 - task.assignID = null;
1166 + pushTextInstance(task.blockedSegment.chunks, node, request.responseState);
1167 return;
1168 }
1169
@@ -1217,9 +1172,7 @@ function renderNodeDestructive(
1172 task.blockedSegment.chunks,
1173 '' + node,
1174 request.responseState,
1220 - task.assignID,
1175 );
1222 - task.assignID = null;
1176 return;
1177 }
1178
@@ -1232,10 +1185,6 @@ function renderNodeDestructive(
1185 );
1186 }
1187 }
1235 -
1236 - // Any other type is assumed to be empty.
1237 - pushEmpty(task.blockedSegment.chunks, request.responseState, task.assignID);
1238 - task.assignID = null;
1188 }
1189
1190 function spawnNewSuspendedTask(
@@ -1261,7 +1210,6 @@ function spawnNewSuspendedTask(
1210 task.abortSet,
1211 task.legacyContext,
1212 task.context,
1264 - task.assignID,
1213 );
1214 if (__DEV__) {
1215 if (task.componentStack !== null) {
@@ -1270,8 +1218,6 @@ function spawnNewSuspendedTask(
1218 newTask.componentStack = task.componentStack.parent;
1219 }
1220 }
1273 - // We've delegated the assignment.
1274 - task.assignID = null;
1221 const ping = newTask.ping;
1222 x.then(ping, ping);
1223 }
@@ -1650,11 +1596,10 @@ function flushSegment(
1596 request.partialBoundaries.push(boundary);
1597 }
1598
1653 - writeStartPendingSuspenseBoundary(
1654 - destination,
1655 - request.responseState,
1656 - boundary.id,
1657 - );
1599 + /// This is the first time we should have referenced this ID.
1600 + const id = (boundary.id = assignSuspenseBoundaryID(request.responseState));
1601 +
1602 + writeStartPendingSuspenseBoundary(destination, request.responseState, id);
1603
1604 // Flush the fallback.
1605 flushSubtree(request, destination, segment);
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+3 -2
@@ -33,9 +33,10 @@ export opaque type OpaqueIDType = mixed;
33 export const isPrimaryRenderer = false;
34
35 export const getChildFormatContext = $$$hostConfig.getChildFormatContext;
36 -export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
36 +export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID =
37 + $$$hostConfig.UNINITIALIZED_SUSPENSE_BOUNDARY_ID;
38 +export const assignSuspenseBoundaryID = $$$hostConfig.assignSuspenseBoundaryID;
39 export const makeServerID = $$$hostConfig.makeServerID;
38 -export const pushEmpty = $$$hostConfig.pushEmpty;
40 export const pushTextInstance = $$$hostConfig.pushTextInstance;
41 export const pushStartInstance = $$$hostConfig.pushStartInstance;
42 export const pushEndInstance = $$$hostConfig.pushEndInstance;