diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 3381005cf5c23..19fde3c4a6059 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -103,6 +103,7 @@ describe('ReactDOMFizzServer', () => {
if (
node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
+ node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden')
) {
@@ -153,7 +154,6 @@ describe('ReactDOMFizzServer', () => {
}
}
- /*
function rejectText(text, error) {
const record = textCache.get(text);
if (record === undefined) {
@@ -169,7 +169,6 @@ describe('ReactDOMFizzServer', () => {
thenable.pings.forEach(t => t());
}
}
- */
function readText(text) {
const record = textCache.get(text);
@@ -800,4 +799,79 @@ describe('ReactDOMFizzServer', () => {
,
);
});
+
+ it('client renders a boundary if it errors before finishing the fallback', async () => {
+ function App({isClient}) {
+ return (
+
+
+
}>
+
+ {isClient ? : }
+
+
+
+
+ );
+ }
+
+ const loggedErrors = [];
+ let controls;
+ await act(async () => {
+ controls = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ {
+ onError(x) {
+ loggedErrors.push(x);
+ },
+ },
+ );
+ controls.startWriting();
+ });
+
+ // We're still showing a fallback.
+
+ // Attempt to hydrate the content.
+ const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+
+ // We're still loading because we're waiting for the server to stream more content.
+ expect(getVisibleChildren(container)).toEqual('Loading root...');
+
+ expect(loggedErrors).toEqual([]);
+
+ const theError = new Error('Test');
+ // Error the content, but we don't have a fallback yet.
+ await act(async () => {
+ rejectText('Hello', theError);
+ });
+
+ expect(loggedErrors).toEqual([theError]);
+
+ // We still can't render it on the client because we haven't unblocked the parent.
+ Scheduler.unstable_flushAll();
+ expect(getVisibleChildren(container)).toEqual('Loading root...');
+
+ // Unblock the loading state
+ await act(async () => {
+ resolveText('Loading...');
+ });
+
+ // Now we're able to show the inner boundary.
+ expect(getVisibleChildren(container)).toEqual(
Loading...
);
+
+ // That will let us client render it instead.
+ Scheduler.unstable_flushAll();
+
+ // The client rendered HTML is now in place.
+ expect(getVisibleChildren(container)).toEqual(
+
+
Hello
+ ,
+ );
+
+ expect(loggedErrors).toEqual([theError]);
+ });
});
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 334d8b38797ad..a27a845f422e7 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -419,17 +419,30 @@ function renderSuspenseBoundary(
task.blockedSegment = parentSegment;
}
+ // This injects an extra segment just to contain an empty tag with an ID.
+ // This means that we're not actually using the assignID anywhere.
+ // TODO: Rethink the assignID approach.
+ pushEmpty(boundarySegment.chunks, request.responseState, newBoundary.id);
+ const innerSegment = createPendingSegment(
+ request,
+ boundarySegment.chunks.length,
+ null,
+ boundarySegment.formatContext,
+ );
+ boundarySegment.status = COMPLETED;
+ boundarySegment.children.push(innerSegment);
+
// We create suspended task for the fallback because we don't want to actually work
// on it yet in case we finish the main content, so we queue for later.
const suspendedFallbackTask = createTask(
request,
fallback,
parentBoundary,
- boundarySegment,
+ innerSegment,
fallbackAbortSet,
task.legacyContext,
task.context,
- newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
+ null,
);
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to task on.