From 81ef5395358b6e0f44a02df618901eaef830fe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 14 Apr 2021 18:39:51 -0400 Subject: [PATCH] Always insert a dummy node with an ID into fallbacks (#21272) --- .../src/__tests__/ReactDOMFizzServer-test.js | 78 ++++++++++++++++++- packages/react-server/src/ReactFizzServer.js | 17 +++- 2 files changed, 91 insertions(+), 4 deletions(-) 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.