Skip to content

Commit 192c039

Browse files
committed
Assign an ID to the first DOM node in a fallback or insert a dummy
1 parent a9e8c41 commit 192c039

File tree

4 files changed

+110
-19
lines changed

4 files changed

+110
-19
lines changed

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import invariant from 'shared/invariant';
2424

2525
// Per response,
2626
export type ResponseState = {
27+
nextSuspenseID: number,
2728
sentCompleteSegmentFunction: boolean,
2829
sentCompleteBoundaryFunction: boolean,
2930
sentClientRenderFunction: boolean,
@@ -32,6 +33,7 @@ export type ResponseState = {
3233
// Allows us to keep track of what we've already written so we can refer back to it.
3334
export function createResponseState(): ResponseState {
3435
return {
36+
nextSuspenseID: 0,
3537
sentCompleteSegmentFunction: false,
3638
sentCompleteBoundaryFunction: false,
3739
sentClientRenderFunction: false,
@@ -42,13 +44,13 @@ export function createResponseState(): ResponseState {
4244
// We can't assign an ID up front because the node we're attaching it to might already
4345
// have one. So we need to lazily use that if it's available.
4446
export type SuspenseBoundaryID = {
45-
id: null | string,
47+
formattedID: null | PrecomputedChunk,
4648
};
4749

4850
export function createSuspenseBoundaryID(
4951
responseState: ResponseState,
5052
): SuspenseBoundaryID {
51-
return {id: null};
53+
return {formattedID: null};
5254
}
5355

5456
function encodeHTMLIDAttribute(value: string): string {
@@ -59,23 +61,86 @@ function encodeHTMLTextNode(text: string): string {
5961
return escapeTextForBrowser(text);
6062
}
6163

64+
function assignAnID(
65+
responseState: ResponseState,
66+
id: SuspenseBoundaryID,
67+
): PrecomputedChunk {
68+
// TODO: This approach doesn't yield deterministic results since this is assigned during render.
69+
const generatedID = responseState.nextSuspenseID++;
70+
return (id.formattedID = stringToPrecomputedChunk(
71+
'B:' + generatedID.toString(16),
72+
));
73+
}
74+
75+
const dummyNode1 = stringToPrecomputedChunk('<span hidden id="');
76+
const dummyNode2 = stringToPrecomputedChunk('"></span>');
77+
78+
function pushDummyNodeWithID(
79+
target: Array<Chunk | PrecomputedChunk>,
80+
responseState: ResponseState,
81+
assignID: SuspenseBoundaryID,
82+
): void {
83+
const id = assignAnID(responseState, assignID);
84+
target.push(dummyNode1, id, dummyNode2);
85+
}
86+
87+
export function pushEmpty(
88+
target: Array<Chunk | PrecomputedChunk>,
89+
responseState: ResponseState,
90+
assignID: null | SuspenseBoundaryID,
91+
): void {
92+
if (assignID !== null) {
93+
pushDummyNodeWithID(target, responseState, assignID);
94+
}
95+
}
96+
6297
export function pushTextInstance(
6398
target: Array<Chunk | PrecomputedChunk>,
6499
text: string,
100+
responseState: ResponseState,
101+
assignID: null | SuspenseBoundaryID,
65102
): void {
103+
if (assignID !== null) {
104+
pushDummyNodeWithID(target, responseState, assignID);
105+
}
66106
target.push(stringToChunk(encodeHTMLTextNode(text)));
67107
}
68108

69109
const startTag1 = stringToPrecomputedChunk('<');
70110
const startTag2 = stringToPrecomputedChunk('>');
71111

112+
const idAttr = stringToPrecomputedChunk(' id="');
113+
const attrEnd = stringToPrecomputedChunk('"');
114+
72115
export function pushStartInstance(
73116
target: Array<Chunk | PrecomputedChunk>,
74117
type: string,
75118
props: Object,
119+
responseState: ResponseState,
120+
assignID: null | SuspenseBoundaryID,
76121
): void {
77122
// TODO: Figure out if it's self closing and everything else.
78-
target.push(startTag1, stringToChunk(type), startTag2);
123+
if (assignID !== null) {
124+
let encodedID;
125+
if (typeof props.id === 'string') {
126+
// We can reuse the existing ID for our purposes.
127+
encodedID = assignID.formattedID = stringToPrecomputedChunk(
128+
encodeHTMLIDAttribute(props.id),
129+
);
130+
} else {
131+
encodedID = assignAnID(responseState, assignID);
132+
}
133+
target.push(
134+
startTag1,
135+
stringToChunk(type),
136+
idAttr,
137+
encodedID,
138+
attrEnd,
139+
startTag2,
140+
);
141+
} else {
142+
target.push(startTag1, stringToChunk(type), startTag2);
143+
}
79144
}
80145

81146
const endTag1 = stringToPrecomputedChunk('</');
@@ -337,13 +402,11 @@ export function writeCompletedBoundaryInstruction(
337402
writeChunk(destination, completeBoundaryScript1Partial);
338403
}
339404
// TODO: Use the identifierPrefix option to make the prefix configurable.
405+
const formattedBoundaryID = boundaryID.formattedID;
340406
invariant(
341-
boundaryID.id !== null,
407+
formattedBoundaryID !== null,
342408
'An ID must have been assigned before we can complete the boundary.',
343409
);
344-
const formattedBoundaryID = stringToChunk(
345-
encodeHTMLIDAttribute(boundaryID.id),
346-
);
347410
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
348411
writeChunk(destination, formattedBoundaryID);
349412
writeChunk(destination, completeBoundaryScript2);
@@ -370,13 +433,11 @@ export function writeClientRenderBoundaryInstruction(
370433
// Future calls can just reuse the same function.
371434
writeChunk(destination, clientRenderScript1Partial);
372435
}
436+
const formattedBoundaryID = boundaryID.formattedID;
373437
invariant(
374-
boundaryID.id !== null,
438+
formattedBoundaryID !== null,
375439
'An ID must have been assigned before we can complete the boundary.',
376440
);
377-
const formattedBoundaryID = stringToPrecomputedChunk(
378-
encodeHTMLIDAttribute(boundaryID.id),
379-
);
380441
writeChunk(destination, formattedBoundaryID);
381442
return writeChunk(destination, clientRenderScript2);
382443
}

packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,25 @@ export type SuspenseBoundaryID = number;
7373
export function createSuspenseBoundaryID(
7474
responseState: ResponseState,
7575
): SuspenseBoundaryID {
76+
// TODO: This is not deterministic since it's created during render.
7677
return responseState.nextSuspenseID++;
7778
}
7879

7980
const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
8081

82+
export function pushEmpty(
83+
target: Array<Chunk | PrecomputedChunk>,
84+
responseState: ResponseState,
85+
assignID: null | SuspenseBoundaryID,
86+
): void {
87+
// This is not used since we don't need to assign any IDs.
88+
}
89+
8190
export function pushTextInstance(
8291
target: Array<Chunk | PrecomputedChunk>,
8392
text: string,
93+
responseState: ResponseState,
94+
assignID: null | SuspenseBoundaryID,
8495
): void {
8596
target.push(
8697
INSTANCE,
@@ -95,6 +106,8 @@ export function pushStartInstance(
95106
target: Array<Chunk | PrecomputedChunk>,
96107
type: string,
97108
props: Object,
109+
responseState: ResponseState,
110+
assignID: null | SuspenseBoundaryID,
98111
): void {
99112
target.push(
100113
INSTANCE,

packages/react-server/src/ReactFizzServer.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
writeClientRenderBoundaryInstruction,
4040
writeCompletedBoundaryInstruction,
4141
writeCompletedSegmentInstruction,
42+
pushEmpty,
4243
pushTextInstance,
4344
pushStartInstance,
4445
pushEndInstance,
@@ -218,15 +219,22 @@ function renderNode(
218219
parentBoundary: Root | SuspenseBoundary,
219220
segment: Segment,
220221
node: ReactNodeList,
222+
assignID: null | SuspenseBoundaryID,
221223
): void {
222224
if (typeof node === 'string') {
223-
pushTextInstance(segment.chunks, node);
225+
pushTextInstance(segment.chunks, node, request.responseState, assignID);
224226
return;
225227
}
226228

227229
if (Array.isArray(node)) {
228-
for (let i = 0; i < node.length; i++) {
229-
renderNode(request, parentBoundary, segment, node[i]);
230+
if (node.length > 0) {
231+
// Only the first node gets assigned an ID.
232+
renderNode(request, parentBoundary, segment, node[0], assignID);
233+
for (let i = 1; i < node.length; i++) {
234+
renderNode(request, parentBoundary, segment, node[i], null);
235+
}
236+
} else {
237+
pushEmpty(segment.chunks, request.responseState, assignID);
230238
}
231239
return;
232240
}
@@ -244,7 +252,7 @@ function renderNode(
244252
if (typeof type === 'function') {
245253
try {
246254
const result = type(props);
247-
renderNode(request, parentBoundary, segment, result);
255+
renderNode(request, parentBoundary, segment, result, assignID);
248256
} catch (x) {
249257
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
250258
// Something suspended, we'll need to create a new segment and resolve it later.
@@ -256,7 +264,7 @@ function renderNode(
256264
node,
257265
parentBoundary,
258266
newSegment,
259-
null,
267+
assignID,
260268
);
261269
const ping = suspendedWork.ping;
262270
x.then(ping, ping);
@@ -267,10 +275,18 @@ function renderNode(
267275
}
268276
}
269277
} else if (typeof type === 'string') {
270-
pushStartInstance(segment.chunks, type, props);
271-
renderNode(request, parentBoundary, segment, props.children);
278+
pushStartInstance(
279+
segment.chunks,
280+
type,
281+
props,
282+
request.responseState,
283+
assignID,
284+
);
285+
renderNode(request, parentBoundary, segment, props.children, null);
272286
pushEndInstance(segment.chunks, type, props);
273287
} else if (type === REACT_SUSPENSE_TYPE) {
288+
// We need to push an "empty" thing here to identify the parent suspense boundary.
289+
pushEmpty(segment.chunks, request.responseState, assignID);
274290
// Each time we enter a suspense boundary, we split out into a new segment for
275291
// the fallback so that we can later replace that segment with the content.
276292
// This also lets us split out the main content even if it doesn't suspend,
@@ -426,7 +442,7 @@ function retryWork(request: Request, work: SuspendedWork): void {
426442
node = element.type(element.props);
427443
}
428444

429-
renderNode(request, boundary, segment, node);
445+
renderNode(request, boundary, segment, node, work.assignID);
430446

431447
completeWork(request, boundary, segment);
432448
} catch (x) {

packages/react-server/src/forks/ReactServerFormatConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export opaque type SuspenseBoundaryID = mixed;
3030

3131
export const createResponseState = $$$hostConfig.createResponseState;
3232
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
33+
export const pushEmpty = $$$hostConfig.pushEmpty;
3334
export const pushTextInstance = $$$hostConfig.pushTextInstance;
3435
export const pushStartInstance = $$$hostConfig.pushStartInstance;
3536
export const pushEndInstance = $$$hostConfig.pushEndInstance;

0 commit comments

Comments
 (0)