Skip to content

Commit 49b206f

Browse files
committed
Add special wrappers around inserted segments depending on their insertion mode
1 parent e748104 commit 49b206f

File tree

6 files changed

+272
-19
lines changed

6 files changed

+272
-19
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ describe('ReactDOMFizzServer', () => {
105105
const props = {};
106106
const attributes = node.attributes;
107107
for (let i = 0; i < attributes.length; i++) {
108+
if (
109+
attributes[i].name === 'id' &&
110+
attributes[i].value.includes(':')
111+
) {
112+
// We assume this is a React added ID that's a non-visual implementation detail.
113+
continue;
114+
}
108115
props[attributes[i].name] = attributes[i].value;
109116
}
110117
props.children = getVisibleChildren(node);
@@ -116,7 +123,7 @@ describe('ReactDOMFizzServer', () => {
116123
node = node.nextSibling;
117124
}
118125
return children.length === 0
119-
? null
126+
? undefined
120127
: children.length === 1
121128
? children[0]
122129
: children;
@@ -423,6 +430,14 @@ describe('ReactDOMFizzServer', () => {
423430
return <col className={readText(className)}>{[]}</col>;
424431
}
425432

433+
function AsyncPath({id}) {
434+
return <path id={readText(id)}>{[]}</path>;
435+
}
436+
437+
function AsyncMi({id}) {
438+
return <mi id={readText(id)}>{[]}</mi>;
439+
}
440+
426441
function App() {
427442
return (
428443
<div>
@@ -437,6 +452,14 @@ describe('ReactDOMFizzServer', () => {
437452
<AsyncCol className="World" />
438453
</colgroup>
439454
</table>
455+
<svg>
456+
<g>
457+
<AsyncPath id="my-path" />
458+
</g>
459+
</svg>
460+
<math>
461+
<AsyncMi id="my-mi" />
462+
</math>
440463
</Suspense>
441464
</div>
442465
);
@@ -464,17 +487,113 @@ describe('ReactDOMFizzServer', () => {
464487
resolveText('World');
465488
});
466489

490+
await act(async () => {
491+
resolveText('my-path');
492+
resolveText('my-mi');
493+
});
494+
467495
expect(getVisibleChildren(container)).toEqual(
468496
<div>
469497
<select>
470498
<option>Hello</option>
471499
</select>
472500
<table>
473501
<colgroup>
474-
<col className="World" />
502+
<col class="World" />
475503
</colgroup>
476504
</table>
505+
<svg>
506+
<g>
507+
<path id="my-path" />
508+
</g>
509+
</svg>
510+
<math>
511+
<mi id="my-mi" />
512+
</math>
477513
</div>,
478514
);
515+
516+
expect(container.querySelector('#my-path').namespaceURI).toBe(
517+
'http://www.w3.org/2000/svg',
518+
);
519+
expect(container.querySelector('#my-mi').namespaceURI).toBe(
520+
'http://www.w3.org/1998/Math/MathML',
521+
);
522+
});
523+
524+
// @gate experimental
525+
it('can resolve async content in table parents', async () => {
526+
function AsyncTableBody({className, children}) {
527+
return <tbody className={readText(className)}>{children}</tbody>;
528+
}
529+
530+
function AsyncTableRow({className, children}) {
531+
return <tr className={readText(className)}>{children}</tr>;
532+
}
533+
534+
function AsyncTableCell({text}) {
535+
return <td>{readText(text)}</td>;
536+
}
537+
538+
function App() {
539+
return (
540+
<table>
541+
<Suspense
542+
fallback={
543+
<tbody>
544+
<tr>
545+
<td>Loading...</td>
546+
</tr>
547+
</tbody>
548+
}>
549+
<AsyncTableBody className="A">
550+
<AsyncTableRow className="B">
551+
<AsyncTableCell text="C" />
552+
</AsyncTableRow>
553+
</AsyncTableBody>
554+
</Suspense>
555+
</table>
556+
);
557+
}
558+
559+
await act(async () => {
560+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
561+
<App />,
562+
writable,
563+
);
564+
startWriting();
565+
});
566+
567+
expect(getVisibleChildren(container)).toEqual(
568+
<table>
569+
<tbody>
570+
<tr>
571+
<td>Loading...</td>
572+
</tr>
573+
</tbody>
574+
</table>,
575+
);
576+
577+
await act(async () => {
578+
resolveText('A');
579+
});
580+
581+
await act(async () => {
582+
resolveText('B');
583+
});
584+
585+
await act(async () => {
586+
resolveText('C');
587+
});
588+
589+
expect(getVisibleChildren(container)).toEqual(
590+
<table>
591+
<tbody class="A">
592+
<tr class="B">
593+
<td>C</td>
594+
</tr>
595+
</tbody>
596+
</table>,
597+
);
479598
});
480599
});

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

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,20 @@ export function pushStartInstance(
228228
startTag2,
229229
);
230230
} else {
231-
target.push(startTag1, stringToChunk(type), startTag2);
231+
target.push(startTag1, stringToChunk(type));
232+
if (props.className) {
233+
target.push(
234+
stringToChunk(
235+
' class="' + encodeHTMLIDAttribute(props.className) + '"',
236+
),
237+
);
238+
}
239+
if (props.id) {
240+
target.push(
241+
stringToChunk(' id="' + encodeHTMLIDAttribute(props.id) + '"'),
242+
);
243+
}
244+
target.push(startTag2);
232245
}
233246
}
234247

@@ -293,23 +306,133 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
293306
return writeChunk(destination, endSuspenseBoundary);
294307
}
295308

296-
const startSegment = stringToPrecomputedChunk('<div hidden id="');
297-
const startSegment2 = stringToPrecomputedChunk('">');
298-
const endSegment = stringToPrecomputedChunk('</div>');
309+
// TODO: div won't work if the Root is SVG or MathML.
310+
const startSegmentRoot = stringToPrecomputedChunk('<div hidden id="');
311+
const startSegmentRoot2 = stringToPrecomputedChunk('">');
312+
const endSegmentRoot = stringToPrecomputedChunk('</div>');
313+
314+
const startSegmentSVG = stringToPrecomputedChunk(
315+
'<svg aria-hidden="true" style="display:none" id="',
316+
);
317+
const startSegmentSVG2 = stringToPrecomputedChunk('">');
318+
const endSegmentSVG = stringToPrecomputedChunk('</svg>');
319+
320+
const startSegmentMathML = stringToPrecomputedChunk(
321+
'<math aria-hidden="true" style="display:none" id="',
322+
);
323+
const startSegmentMathML2 = stringToPrecomputedChunk('">');
324+
const endSegmentMathML = stringToPrecomputedChunk('</math>');
325+
326+
const startSegmentTable = stringToPrecomputedChunk('<table hidden id="');
327+
const startSegmentTable2 = stringToPrecomputedChunk('">');
328+
const endSegmentTable = stringToPrecomputedChunk('</table>');
329+
330+
const startSegmentTableBody = stringToPrecomputedChunk(
331+
'<table hidden><tbody id="',
332+
);
333+
const startSegmentTableBody2 = stringToPrecomputedChunk('">');
334+
const endSegmentTableBody = stringToPrecomputedChunk('</tbody></table>');
335+
336+
const startSegmentTableRow = stringToPrecomputedChunk('<table hidden><tr id="');
337+
const startSegmentTableRow2 = stringToPrecomputedChunk('">');
338+
const endSegmentTableRow = stringToPrecomputedChunk('</tr></table>');
339+
340+
const startSegmentColGroup = stringToPrecomputedChunk(
341+
'<table hidden><colgroup id="',
342+
);
343+
const startSegmentColGroup2 = stringToPrecomputedChunk('">');
344+
const endSegmentColGroup = stringToPrecomputedChunk('</colgroup></table>');
345+
299346
export function writeStartSegment(
300347
destination: Destination,
301348
responseState: ResponseState,
349+
formatContext: FormatContext,
302350
id: number,
303351
): boolean {
304-
// TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
305-
writeChunk(destination, startSegment);
306-
writeChunk(destination, responseState.segmentPrefix);
307-
const formattedID = stringToChunk(id.toString(16));
308-
writeChunk(destination, formattedID);
309-
return writeChunk(destination, startSegment2);
352+
switch (formatContext.insertionMode) {
353+
case ROOT_MODE:
354+
case HTML_MODE: {
355+
writeChunk(destination, startSegmentRoot);
356+
writeChunk(destination, responseState.segmentPrefix);
357+
writeChunk(destination, stringToChunk(id.toString(16)));
358+
return writeChunk(destination, startSegmentRoot2);
359+
}
360+
case SVG_MODE: {
361+
writeChunk(destination, startSegmentSVG);
362+
writeChunk(destination, responseState.segmentPrefix);
363+
writeChunk(destination, stringToChunk(id.toString(16)));
364+
return writeChunk(destination, startSegmentSVG2);
365+
}
366+
case MATHML_MODE: {
367+
writeChunk(destination, startSegmentMathML);
368+
writeChunk(destination, responseState.segmentPrefix);
369+
writeChunk(destination, stringToChunk(id.toString(16)));
370+
return writeChunk(destination, startSegmentMathML2);
371+
}
372+
case HTML_TABLE_MODE: {
373+
writeChunk(destination, startSegmentTable);
374+
writeChunk(destination, responseState.segmentPrefix);
375+
writeChunk(destination, stringToChunk(id.toString(16)));
376+
return writeChunk(destination, startSegmentTable2);
377+
}
378+
// TODO: For the rest of these, there will be extra wrapper nodes that never
379+
// get deleted from the document. We need to delete the table too as part
380+
// of the injected scripts. They are invisible though so it's not too terrible
381+
// and it's kind of an edge case to suspend in a table. Totally supported though.
382+
case HTML_TABLE_BODY_MODE: {
383+
writeChunk(destination, startSegmentTableBody);
384+
writeChunk(destination, responseState.segmentPrefix);
385+
writeChunk(destination, stringToChunk(id.toString(16)));
386+
return writeChunk(destination, startSegmentTableBody2);
387+
}
388+
case HTML_TABLE_ROW_MODE: {
389+
writeChunk(destination, startSegmentTableRow);
390+
writeChunk(destination, responseState.segmentPrefix);
391+
writeChunk(destination, stringToChunk(id.toString(16)));
392+
return writeChunk(destination, startSegmentTableRow2);
393+
}
394+
case HTML_COLGROUP_MODE: {
395+
writeChunk(destination, startSegmentColGroup);
396+
writeChunk(destination, responseState.segmentPrefix);
397+
writeChunk(destination, stringToChunk(id.toString(16)));
398+
return writeChunk(destination, startSegmentColGroup2);
399+
}
400+
default: {
401+
invariant(false, 'Unknown insertion mode. This is a bug in React.');
402+
}
403+
}
310404
}
311-
export function writeEndSegment(destination: Destination): boolean {
312-
return writeChunk(destination, endSegment);
405+
export function writeEndSegment(
406+
destination: Destination,
407+
formatContext: FormatContext,
408+
): boolean {
409+
switch (formatContext.insertionMode) {
410+
case ROOT_MODE:
411+
case HTML_MODE: {
412+
return writeChunk(destination, endSegmentRoot);
413+
}
414+
case SVG_MODE: {
415+
return writeChunk(destination, endSegmentSVG);
416+
}
417+
case MATHML_MODE: {
418+
return writeChunk(destination, endSegmentMathML);
419+
}
420+
case HTML_TABLE_MODE: {
421+
return writeChunk(destination, endSegmentTable);
422+
}
423+
case HTML_TABLE_BODY_MODE: {
424+
return writeChunk(destination, endSegmentTableBody);
425+
}
426+
case HTML_TABLE_ROW_MODE: {
427+
return writeChunk(destination, endSegmentTableRow);
428+
}
429+
case HTML_COLGROUP_MODE: {
430+
return writeChunk(destination, endSegmentColGroup);
431+
}
432+
default: {
433+
invariant(false, 'Unknown insertion mode. This is a bug in React.');
434+
}
435+
}
313436
}
314437

315438
// Instruction Set

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,16 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
208208
export function writeStartSegment(
209209
destination: Destination,
210210
responseState: ResponseState,
211+
formatContext: FormatContext,
211212
id: number,
212213
): boolean {
213214
writeChunk(destination, SEGMENT);
214215
return writeChunk(destination, formatID(id));
215216
}
216-
export function writeEndSegment(destination: Destination): boolean {
217+
export function writeEndSegment(
218+
destination: Destination,
219+
formatContext: FormatContext,
220+
): boolean {
217221
return writeChunk(destination, END);
218222
}
219223

packages/react-noop-renderer/src/ReactNoopServer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ const ReactNoopServer = ReactFizzServer({
161161
writeStartSegment(
162162
destination: Destination,
163163
responseState: ResponseState,
164+
formatContext: null,
164165
id: number,
165166
): boolean {
166167
const segment = {
@@ -172,7 +173,7 @@ const ReactNoopServer = ReactFizzServer({
172173
}
173174
destination.stack.push(segment);
174175
},
175-
writeEndSegment(destination: Destination): boolean {
176+
writeEndSegment(destination: Destination, formatContext: null): boolean {
176177
destination.stack.pop();
177178
},
178179

packages/react-server/src/ReactFizzServer.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,14 @@ function flushSegmentContainer(
786786
destination: Destination,
787787
segment: Segment,
788788
): boolean {
789-
writeStartSegment(destination, request.responseState, segment.id);
789+
writeStartSegment(
790+
destination,
791+
request.responseState,
792+
segment.formatContext,
793+
segment.id,
794+
);
790795
flushSegment(request, destination, segment);
791-
return writeEndSegment(destination);
796+
return writeEndSegment(destination, segment.formatContext);
792797
}
793798

794799
function flushCompletedBoundary(

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,5 +384,6 @@
384384
"393": "Cache cannot be refreshed during server rendering.",
385385
"394": "startTransition cannot be called during server rendering.",
386386
"395": "An ID must have been assigned before we can complete the boundary.",
387-
"396": "More boundaries or placeholders than we expected to ever emit."
387+
"396": "More boundaries or placeholders than we expected to ever emit.",
388+
"397": "Unknown insertion mode. This is a bug in React."
388389
}

0 commit comments

Comments
 (0)