Skip to content

[Fizz] Don't handle errors in completeBoundary instruction #33073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -4482,14 +4482,14 @@ export function writeCompletedSegmentInstruction(
}
}

const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
completeBoundaryFunction,
);
const completeBoundaryScript1Full = stringToPrecomputedChunk(
completeBoundaryFunction + '$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');

const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
completeBoundaryFunction + styleInsertionFunction + '$RR("',
);
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
styleInsertionFunction + '$RR("',
);
Expand Down Expand Up @@ -4531,19 +4531,27 @@ export function writeCompletedBoundaryInstruction(
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (requiresStyleInsertion) {
if (
(resumableState.instructions & SentClientRenderFunction) ===
NothingSent
) {
// The completeBoundaryWithStyles function depends on the client render function.
resumableState.instructions |= SentClientRenderFunction;
writeChunk(destination, clientRenderScriptFunctionOnly);
}
if (
(resumableState.instructions & SentCompleteBoundaryFunction) ===
NothingSent
) {
resumableState.instructions |=
SentStyleInsertionFunction | SentCompleteBoundaryFunction;
writeChunk(destination, completeBoundaryWithStylesScript1FullBoth);
} else if (
// The completeBoundaryWithStyles function depends on the complete boundary function.
resumableState.instructions |= SentCompleteBoundaryFunction;
writeChunk(destination, completeBoundaryScriptFunctionOnly);
}
if (
(resumableState.instructions & SentStyleInsertionFunction) ===
NothingSent
) {
resumableState.instructions |= SentStyleInsertionFunction;

writeChunk(destination, completeBoundaryWithStylesScript1FullPartial);
} else {
writeChunk(destination, completeBoundaryWithStylesScript1Partial);
Expand Down Expand Up @@ -4608,6 +4616,9 @@ export function writeCompletedBoundaryInstruction(
return writeBootstrap(destination, renderState) && writeMore;
}

const clientRenderScriptFunctionOnly =
stringToPrecomputedChunk(clientRenderFunction);

const clientRenderScript1Full = stringToPrecomputedChunk(
clientRenderFunction + ';$RX("',
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function clientRenderBoundary(
}
}

export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
export function completeBoundary(suspenseBoundaryID, contentID) {
const contentNode = document.getElementById(contentID);
if (!contentNode) {
// If the client has failed hydration we may have already deleted the streaming
Expand All @@ -70,52 +70,47 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
// Find the boundary around the fallback. This is always the previous node.
const suspenseNode = suspenseIdNode.previousSibling;

if (!errorDigest) {
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
const parentInstance = suspenseNode.parentNode;
let node = suspenseNode.nextSibling;
let depth = 0;
do {
if (node && node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
break;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
const parentInstance = suspenseNode.parentNode;
let node = suspenseNode.nextSibling;
let depth = 0;
do {
if (node && node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
break;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
}
}

const nextNode = node.nextSibling;
parentInstance.removeChild(node);
node = nextNode;
} while (node);

const endOfBoundary = node;
const nextNode = node.nextSibling;
parentInstance.removeChild(node);
node = nextNode;
} while (node);

// Insert all the children from the contentNode between the start and end of suspense boundary.
while (contentNode.firstChild) {
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
}
const endOfBoundary = node;

suspenseNode.data = SUSPENSE_START_DATA;
} else {
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
suspenseIdNode.setAttribute('data-dgst', errorDigest);
// Insert all the children from the contentNode between the start and end of suspense boundary.
while (contentNode.firstChild) {
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
}

suspenseNode.data = SUSPENSE_START_DATA;

if (suspenseNode['_reactRetry']) {
suspenseNode['_reactRetry']();
}
Expand Down Expand Up @@ -234,13 +229,8 @@ export function completeBoundaryWithStyles(
}

Promise.all(dependencies).then(
window['$RC'].bind(null, suspenseBoundaryID, contentID, ''),
window['$RC'].bind(
null,
suspenseBoundaryID,
contentID,
'Resource failed to load',
),
window['$RC'].bind(null, suspenseBoundaryID, contentID),
window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1630,14 +1630,14 @@ describe('ReactDOMFizzStaticBrowser', () => {
// We are mostly just trying to assert that no preload for our stylesheet was emitted
// prior to sending the segment the stylesheet was for. This test is asserting this
// because the boundary complete instruction is sent when we are writing the
const instructionIndex = result.indexOf('$RC');
const instructionIndex = result.indexOf('$RX');
expect(instructionIndex > -1).toBe(true);
const slice = result.slice(0, instructionIndex + '$RC'.length);
const slice = result.slice(0, instructionIndex + '$RX'.length);

expect(slice).toBe(
'<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body>hello<!--$?--><template id="B:1"></template><!--/$--><template id="«R»"></template>' +
'<div hidden id="S:1">world<!-- --></div><script>$RC',
'<div hidden id="S:1">world<!-- --></div><script>$RX',
);
});

Expand Down
6 changes: 2 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1265,9 +1265,7 @@ body {
const suspenseInstance = boundaryTemplateInstance.previousSibling;

expect(suspenseInstance.data).toEqual('$!');
expect(boundaryTemplateInstance.dataset.dgst).toBe(
'Resource failed to load',
);
expect(boundaryTemplateInstance.dataset.dgst).toBe('CSS failed to load');

expect(getMeaningfulChildren(document)).toEqual(
<html>
Expand Down Expand Up @@ -1313,7 +1311,7 @@ body {
);
expect(errors).toEqual([
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
'Resource failed to load',
'CSS failed to load',
]);
});

Expand Down
Loading