@samitouri / QOS-React / commits / 266c26ad45

Emit reactroot attribute on the first element we discover (#21154)

This may not be the first root element if the root is a fragment and the second one unsuspends first. But this tag doesn't work well for root fragments anyway.

Sebastian Markbåge committed Apr 14, 2021 at 18:09 UTC 266c26ad45c977ecf67e9359809c37da1489ca63
4 files changed +37 -6
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+4
@@ -116,6 +116,10 @@ describe('ReactDOMFizzServer', () => {
116 // We assume this is a React added ID that's a non-visual implementation detail.
117 continue;
118 }
119 + if (attributes[i].name === 'data-reactroot') {
120 + // We ignore React injected attributes.
121 + continue;
122 + }
123 props[attributes[i].name] = attributes[i].value;
124 }
125 props.children = getVisibleChildren(node);
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+4 -2
@@ -55,7 +55,9 @@ describe('ReactDOMFizzServer', () => {
55 <div>hello world</div>,
56 );
57 const result = await readResult(stream);
58 - expect(result).toMatchInlineSnapshot(`"<div>hello world<!-- --></div>"`);
58 + expect(result).toMatchInlineSnapshot(
59 + `"<div data-reactroot=\\"\\">hello world<!-- --></div>"`,
60 + );
61 });
62
63 // @gate experimental
@@ -94,7 +96,7 @@ describe('ReactDOMFizzServer', () => {
96
97 const result = await readResult(stream);
98 expect(result).toMatchInlineSnapshot(
97 - `"<div><!--$-->Done<!-- --><!--/$--></div>"`,
99 + `"<div data-reactroot=\\"\\"><!--$-->Done<!-- --><!--/$--></div>"`,
100 );
101 });
102
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+3 -3
@@ -66,7 +66,7 @@ describe('ReactDOMFizzServer', () => {
66 startWriting();
67 jest.runAllTimers();
68 expect(output.result).toMatchInlineSnapshot(
69 - `"<div>hello world<!-- --></div>"`,
69 + `"<div data-reactroot=\\"\\">hello world<!-- --></div>"`,
70 );
71 });
72
@@ -84,7 +84,7 @@ describe('ReactDOMFizzServer', () => {
84 // Then React starts writing.
85 startWriting();
86 expect(output.result).toMatchInlineSnapshot(
87 - `"<!doctype html><html><head><title>test</title><head><body><div>hello world<!-- --></div>"`,
87 + `"<!doctype html><html><head><title>test</title><head><body><div data-reactroot=\\"\\">hello world<!-- --></div>"`,
88 );
89 });
90
@@ -132,7 +132,7 @@ describe('ReactDOMFizzServer', () => {
132 // Then React starts writing.
133 startWriting();
134 expect(output.result).toMatchInlineSnapshot(
135 - `"<!doctype html><html><head><title>test</title><head><body><div><!--$-->Done<!-- --><!--/$--></div>"`,
135 + `"<!doctype html><html><head><title>test</title><head><body><div data-reactroot=\\"\\"><!--$-->Done<!-- --><!--/$--></div>"`,
136 );
137 });
138
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+26 -1
@@ -32,6 +32,7 @@ import {
32 OVERLOADED_BOOLEAN,
33 NUMERIC,
34 POSITIVE_NUMERIC,
35 + ROOT_ATTRIBUTE_NAME,
36 } from '../shared/DOMProperty';
37 import {isUnitlessNumber} from '../shared/CSSProperty';
38
@@ -63,6 +64,7 @@ export type ResponseState = {
64 sentCompleteSegmentFunction: boolean,
65 sentCompleteBoundaryFunction: boolean,
66 sentClientRenderFunction: boolean,
67 + hasEmittedRoot: boolean,
68 };
69
70 // Allows us to keep track of what we've already written so we can refer back to it.
@@ -79,6 +81,7 @@ export function createResponseState(
81 sentCompleteSegmentFunction: false,
82 sentCompleteBoundaryFunction: false,
83 sentClientRenderFunction: false,
84 + hasEmittedRoot: false,
85 };
86 }
87
@@ -99,7 +102,7 @@ type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
102
103 // Lets us keep track of contextual state and pick it back up after suspending.
104 export type FormatContext = {
102 - insertionMode: InsertionMode, // root/svg/html/mathml/table
105 + insertionMode: InsertionMode, // svg/html/mathml/table
106 selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
107 };
108
@@ -508,6 +511,19 @@ const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');
511 const idAttr = stringToPrecomputedChunk(' id="');
512 const attrEnd = stringToPrecomputedChunk('"');
513
514 +const reactRootAttribute = stringToPrecomputedChunk(
515 + ' ' + ROOT_ATTRIBUTE_NAME + '=""',
516 +);
517 +function pushReactRoot(
518 + target: Array<Chunk | PrecomputedChunk>,
519 + responseState: ResponseState,
520 +): void {
521 + if (!responseState.hasEmittedRoot) {
522 + responseState.hasEmittedRoot = true;
523 + target.push(reactRootAttribute);
524 + }
525 +}
526 +
527 function pushID(
528 target: Array<Chunk | PrecomputedChunk>,
529 responseState: ResponseState,
@@ -639,6 +655,7 @@ function pushStartSelect(
655 if (assignID !== null) {
656 pushID(target, responseState, assignID, props.id);
657 }
658 + pushReactRoot(target, responseState);
659
660 target.push(endOfStartTag);
661 pushInnerHTML(target, innerHTML, children);
@@ -752,6 +769,7 @@ function pushStartOption(
769 if (assignID !== null) {
770 pushID(target, responseState, assignID, props.id);
771 }
772 + pushReactRoot(target, responseState);
773
774 target.push(endOfStartTag);
775 return children;
@@ -839,6 +857,7 @@ function pushInput(
857 if (assignID !== null) {
858 pushID(target, responseState, assignID, props.id);
859 }
860 + pushReactRoot(target, responseState);
861
862 target.push(endOfStartTagSelfClosing);
863 return null;
@@ -903,6 +922,7 @@ function pushStartTextArea(
922 if (assignID !== null) {
923 pushID(target, responseState, assignID, props.id);
924 }
925 + pushReactRoot(target, responseState);
926
927 target.push(endOfStartTag);
928
@@ -979,6 +999,7 @@ function pushSelfClosing(
999 if (assignID !== null) {
1000 pushID(target, responseState, assignID, props.id);
1001 }
1002 + pushReactRoot(target, responseState);
1003
1004 target.push(endOfStartTagSelfClosing);
1005 return null;
@@ -1015,6 +1036,7 @@ function pushStartMenuItem(
1036 if (assignID !== null) {
1037 pushID(target, responseState, assignID, props.id);
1038 }
1039 + pushReactRoot(target, responseState);
1040
1041 target.push(endOfStartTag);
1042 return null;
@@ -1053,6 +1075,7 @@ function pushStartGenericElement(
1075 if (assignID !== null) {
1076 pushID(target, responseState, assignID, props.id);
1077 }
1078 + pushReactRoot(target, responseState);
1079
1080 target.push(endOfStartTag);
1081 pushInnerHTML(target, innerHTML, children);
@@ -1111,6 +1134,7 @@ function pushStartCustomElement(
1134 if (assignID !== null) {
1135 pushID(target, responseState, assignID, props.id);
1136 }
1137 + pushReactRoot(target, responseState);
1138
1139 target.push(endOfStartTag);
1140 pushInnerHTML(target, innerHTML, children);
@@ -1152,6 +1176,7 @@ function pushStartPreformattedElement(
1176 if (assignID !== null) {
1177 pushID(target, responseState, assignID, props.id);
1178 }
1179 + pushReactRoot(target, responseState);
1180
1181 target.push(endOfStartTag);
1182