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