Skip to content

Commit 266c26a

Browse files
authored
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.
1 parent a4a940d commit 266c26a

File tree

4 files changed

+37
-6
lines changed

4 files changed

+37
-6
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ describe('ReactDOMFizzServer', () => {
116116
// We assume this is a React added ID that's a non-visual implementation detail.
117117
continue;
118118
}
119+
if (attributes[i].name === 'data-reactroot') {
120+
// We ignore React injected attributes.
121+
continue;
122+
}
119123
props[attributes[i].name] = attributes[i].value;
120124
}
121125
props.children = getVisibleChildren(node);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ describe('ReactDOMFizzServer', () => {
5555
<div>hello world</div>,
5656
);
5757
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+
);
5961
});
6062

6163
// @gate experimental
@@ -94,7 +96,7 @@ describe('ReactDOMFizzServer', () => {
9496

9597
const result = await readResult(stream);
9698
expect(result).toMatchInlineSnapshot(
97-
`"<div><!--$-->Done<!-- --><!--/$--></div>"`,
99+
`"<div data-reactroot=\\"\\"><!--$-->Done<!-- --><!--/$--></div>"`,
98100
);
99101
});
100102

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('ReactDOMFizzServer', () => {
6666
startWriting();
6767
jest.runAllTimers();
6868
expect(output.result).toMatchInlineSnapshot(
69-
`"<div>hello world<!-- --></div>"`,
69+
`"<div data-reactroot=\\"\\">hello world<!-- --></div>"`,
7070
);
7171
});
7272

@@ -84,7 +84,7 @@ describe('ReactDOMFizzServer', () => {
8484
// Then React starts writing.
8585
startWriting();
8686
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>"`,
8888
);
8989
});
9090

@@ -132,7 +132,7 @@ describe('ReactDOMFizzServer', () => {
132132
// Then React starts writing.
133133
startWriting();
134134
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>"`,
136136
);
137137
});
138138

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
OVERLOADED_BOOLEAN,
3333
NUMERIC,
3434
POSITIVE_NUMERIC,
35+
ROOT_ATTRIBUTE_NAME,
3536
} from '../shared/DOMProperty';
3637
import {isUnitlessNumber} from '../shared/CSSProperty';
3738

@@ -63,6 +64,7 @@ export type ResponseState = {
6364
sentCompleteSegmentFunction: boolean,
6465
sentCompleteBoundaryFunction: boolean,
6566
sentClientRenderFunction: boolean,
67+
hasEmittedRoot: boolean,
6668
};
6769

6870
// 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(
7981
sentCompleteSegmentFunction: false,
8082
sentCompleteBoundaryFunction: false,
8183
sentClientRenderFunction: false,
84+
hasEmittedRoot: false,
8285
};
8386
}
8487

@@ -99,7 +102,7 @@ type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
99102

100103
// Lets us keep track of contextual state and pick it back up after suspending.
101104
export type FormatContext = {
102-
insertionMode: InsertionMode, // root/svg/html/mathml/table
105+
insertionMode: InsertionMode, // svg/html/mathml/table
103106
selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
104107
};
105108

@@ -508,6 +511,19 @@ const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');
508511
const idAttr = stringToPrecomputedChunk(' id="');
509512
const attrEnd = stringToPrecomputedChunk('"');
510513

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+
511527
function pushID(
512528
target: Array<Chunk | PrecomputedChunk>,
513529
responseState: ResponseState,
@@ -639,6 +655,7 @@ function pushStartSelect(
639655
if (assignID !== null) {
640656
pushID(target, responseState, assignID, props.id);
641657
}
658+
pushReactRoot(target, responseState);
642659

643660
target.push(endOfStartTag);
644661
pushInnerHTML(target, innerHTML, children);
@@ -752,6 +769,7 @@ function pushStartOption(
752769
if (assignID !== null) {
753770
pushID(target, responseState, assignID, props.id);
754771
}
772+
pushReactRoot(target, responseState);
755773

756774
target.push(endOfStartTag);
757775
return children;
@@ -839,6 +857,7 @@ function pushInput(
839857
if (assignID !== null) {
840858
pushID(target, responseState, assignID, props.id);
841859
}
860+
pushReactRoot(target, responseState);
842861

843862
target.push(endOfStartTagSelfClosing);
844863
return null;
@@ -903,6 +922,7 @@ function pushStartTextArea(
903922
if (assignID !== null) {
904923
pushID(target, responseState, assignID, props.id);
905924
}
925+
pushReactRoot(target, responseState);
906926

907927
target.push(endOfStartTag);
908928

@@ -979,6 +999,7 @@ function pushSelfClosing(
979999
if (assignID !== null) {
9801000
pushID(target, responseState, assignID, props.id);
9811001
}
1002+
pushReactRoot(target, responseState);
9821003

9831004
target.push(endOfStartTagSelfClosing);
9841005
return null;
@@ -1015,6 +1036,7 @@ function pushStartMenuItem(
10151036
if (assignID !== null) {
10161037
pushID(target, responseState, assignID, props.id);
10171038
}
1039+
pushReactRoot(target, responseState);
10181040

10191041
target.push(endOfStartTag);
10201042
return null;
@@ -1053,6 +1075,7 @@ function pushStartGenericElement(
10531075
if (assignID !== null) {
10541076
pushID(target, responseState, assignID, props.id);
10551077
}
1078+
pushReactRoot(target, responseState);
10561079

10571080
target.push(endOfStartTag);
10581081
pushInnerHTML(target, innerHTML, children);
@@ -1111,6 +1134,7 @@ function pushStartCustomElement(
11111134
if (assignID !== null) {
11121135
pushID(target, responseState, assignID, props.id);
11131136
}
1137+
pushReactRoot(target, responseState);
11141138

11151139
target.push(endOfStartTag);
11161140
pushInnerHTML(target, innerHTML, children);
@@ -1152,6 +1176,7 @@ function pushStartPreformattedElement(
11521176
if (assignID !== null) {
11531177
pushID(target, responseState, assignID, props.id);
11541178
}
1179+
pushReactRoot(target, responseState);
11551180

11561181
target.push(endOfStartTag);
11571182

0 commit comments

Comments
 (0)