Skip to content

Commit

Permalink
Assign an ID to the first DOM node in a fallback or insert a dummy
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Mar 16, 2021
1 parent a9e8c41 commit 99591ea
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 19 deletions.
81 changes: 70 additions & 11 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import invariant from 'shared/invariant';

// Per response,
export type ResponseState = {
nextSuspenseID: number,
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean,
Expand All @@ -32,6 +33,7 @@ export type ResponseState = {
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(): ResponseState {
return {
nextSuspenseID: 0,
sentCompleteSegmentFunction: false,
sentCompleteBoundaryFunction: false,
sentClientRenderFunction: false,
Expand All @@ -42,13 +44,13 @@ export function createResponseState(): ResponseState {
// We can't assign an ID up front because the node we're attaching it to might already
// have one. So we need to lazily use that if it's available.
export type SuspenseBoundaryID = {
id: null | string,
formattedID: null | PrecomputedChunk,
};

export function createSuspenseBoundaryID(
responseState: ResponseState,
): SuspenseBoundaryID {
return {id: null};
return {formattedID: null};
}

function encodeHTMLIDAttribute(value: string): string {
Expand All @@ -59,23 +61,84 @@ function encodeHTMLTextNode(text: string): string {
return escapeTextForBrowser(text);
}

function assignAnID(
responseState: ResponseState,
id: SuspenseBoundaryID,
): PrecomputedChunk {
// TODO: This approach doesn't yield deterministic results since this is assigned during render.
const generatedID = responseState.nextSuspenseID++;
return (id.formattedID = stringToPrecomputedChunk(generatedID.toString(16)));
}

const dummyNode1 = stringToPrecomputedChunk('<span hidden id="');
const dummyNode2 = stringToPrecomputedChunk('"></span>');

function pushDummyNodeWithID(
target: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
assignID: SuspenseBoundaryID,
): void {
const id = assignAnID(responseState, assignID);
target.push(dummyNode1, id, dummyNode2);
}

export function pushEmpty(
target: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
if (assignID !== null) {
pushDummyNodeWithID(target, responseState, assignID);
}
}

export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
if (assignID !== null) {
pushDummyNodeWithID(target, responseState, assignID);
}
target.push(stringToChunk(encodeHTMLTextNode(text)));
}

const startTag1 = stringToPrecomputedChunk('<');
const startTag2 = stringToPrecomputedChunk('>');

const idAttr = stringToPrecomputedChunk(' id="');
const attrEnd = stringToPrecomputedChunk('"');

export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
// TODO: Figure out if it's self closing and everything else.
target.push(startTag1, stringToChunk(type), startTag2);
if (assignID !== null) {
let encodedID;
if (typeof props.id === 'string') {
// We can reuse the existing ID for our purposes.
encodedID = assignID.formattedID = stringToPrecomputedChunk(
encodeHTMLIDAttribute(props.id),
);
} else {
encodedID = assignAnID(responseState, assignID);
}
target.push(
startTag1,
stringToChunk(type),
idAttr,
encodedID,
attrEnd,
startTag2,
);
} else {
target.push(startTag1, stringToChunk(type), startTag2);
}
}

const endTag1 = stringToPrecomputedChunk('</');
Expand Down Expand Up @@ -337,13 +400,11 @@ export function writeCompletedBoundaryInstruction(
writeChunk(destination, completeBoundaryScript1Partial);
}
// TODO: Use the identifierPrefix option to make the prefix configurable.
const formattedBoundaryID = boundaryID.formattedID;
invariant(
boundaryID.id !== null,
formattedBoundaryID !== null,
'An ID must have been assigned before we can complete the boundary.',
);
const formattedBoundaryID = stringToChunk(
encodeHTMLIDAttribute(boundaryID.id),
);
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, formattedBoundaryID);
writeChunk(destination, completeBoundaryScript2);
Expand All @@ -370,13 +431,11 @@ export function writeClientRenderBoundaryInstruction(
// Future calls can just reuse the same function.
writeChunk(destination, clientRenderScript1Partial);
}
const formattedBoundaryID = boundaryID.formattedID;
invariant(
boundaryID.id !== null,
formattedBoundaryID !== null,
'An ID must have been assigned before we can complete the boundary.',
);
const formattedBoundaryID = stringToPrecomputedChunk(
encodeHTMLIDAttribute(boundaryID.id),
);
writeChunk(destination, formattedBoundaryID);
return writeChunk(destination, clientRenderScript2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,25 @@ export type SuspenseBoundaryID = number;
export function createSuspenseBoundaryID(
responseState: ResponseState,
): SuspenseBoundaryID {
// TODO: This is not deterministic since it's created during render.
return responseState.nextSuspenseID++;
}

const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');

export function pushEmpty(
target: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
// This is not used since we don't need to assign any IDs.
}

export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
target.push(
INSTANCE,
Expand All @@ -95,6 +106,8 @@ export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
assignID: null | SuspenseBoundaryID,
): void {
target.push(
INSTANCE,
Expand Down
32 changes: 24 additions & 8 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
writeClientRenderBoundaryInstruction,
writeCompletedBoundaryInstruction,
writeCompletedSegmentInstruction,
pushEmpty,
pushTextInstance,
pushStartInstance,
pushEndInstance,
Expand Down Expand Up @@ -218,15 +219,22 @@ function renderNode(
parentBoundary: Root | SuspenseBoundary,
segment: Segment,
node: ReactNodeList,
assignID: null | SuspenseBoundaryID,
): void {
if (typeof node === 'string') {
pushTextInstance(segment.chunks, node);
pushTextInstance(segment.chunks, node, request.responseState, assignID);
return;
}

if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
renderNode(request, parentBoundary, segment, node[i]);
if (node.length > 0) {
// Only the first node gets assigned an ID.
renderNode(request, parentBoundary, segment, node[0], assignID);
for (let i = 1; i < node.length; i++) {
renderNode(request, parentBoundary, segment, node[i], null);
}
} else {
pushEmpty(segment.chunks, request.responseState, assignID);
}
return;
}
Expand All @@ -244,7 +252,7 @@ function renderNode(
if (typeof type === 'function') {
try {
const result = type(props);
renderNode(request, parentBoundary, segment, result);
renderNode(request, parentBoundary, segment, result, assignID);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Something suspended, we'll need to create a new segment and resolve it later.
Expand All @@ -256,7 +264,7 @@ function renderNode(
node,
parentBoundary,
newSegment,
null,
assignID,
);
const ping = suspendedWork.ping;
x.then(ping, ping);
Expand All @@ -267,10 +275,18 @@ function renderNode(
}
}
} else if (typeof type === 'string') {
pushStartInstance(segment.chunks, type, props);
renderNode(request, parentBoundary, segment, props.children);
pushStartInstance(
segment.chunks,
type,
props,
request.responseState,
assignID,
);
renderNode(request, parentBoundary, segment, props.children, null);
pushEndInstance(segment.chunks, type, props);
} else if (type === REACT_SUSPENSE_TYPE) {
// We need to push an "empty" thing here to identify the parent suspense boundary.
pushEmpty(segment.chunks, request.responseState, assignID);
// Each time we enter a suspense boundary, we split out into a new segment for
// the fallback so that we can later replace that segment with the content.
// This also lets us split out the main content even if it doesn't suspend,
Expand Down Expand Up @@ -426,7 +442,7 @@ function retryWork(request: Request, work: SuspendedWork): void {
node = element.type(element.props);
}

renderNode(request, boundary, segment, node);
renderNode(request, boundary, segment, node, work.assignID);

completeWork(request, boundary, segment);
} catch (x) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export opaque type SuspenseBoundaryID = mixed;

export const createResponseState = $$$hostConfig.createResponseState;
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
export const pushEmpty = $$$hostConfig.pushEmpty;
export const pushTextInstance = $$$hostConfig.pushTextInstance;
export const pushStartInstance = $$$hostConfig.pushStartInstance;
export const pushEndInstance = $$$hostConfig.pushEndInstance;
Expand Down

0 comments on commit 99591ea

Please sign in to comment.