Repository: facebook/react
Pull request: [Fizz] Name content inside "Suspense fallback"

Content in Suspense fallbacks are really not considered part of the Suspense but since it does have some behavior it should be marked somehow separately from the Suspense content.

A follow up would be to do the same in Fiber.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -816,6 +816,52 @@ describe('ReactDOMFizzServer', () => {
     expect(loggedErrors).toEqual([theError]);
   });
 
+  it('should have special stacks if Suspense fallback', async () => {
+    const infinitePromise = new Promise(() => {});
+    const InfiniteComponent = React.lazy(() => {
+      return infinitePromise;
+    });
+
+    function Throw({text}) {
+      throw new Error(text);
+    }
+
+    function App() {
+      return (
+        <Suspense fallback="Loading">
+          <div>
+            <Suspense fallback={<Throw text="Bye" />}>
+              <InfiniteComponent text="Hi" />
+            </Suspense>
+          </div>
+        </Suspense>
+      );
+    }
+
+    const loggedErrors = [];
+    function onError(x, errorInfo) {
+      loggedErrors.push({
+        message: x.message,
+        componentStack: errorInfo.componentStack,
+      });
+      return 'Hash of (' + x.message + ')';
+    }
+    loggedErrors.length = 0;
+
+    await act(() => {
+      const {pipe} = renderToPipeableStream(<App />, {
+        onError,
+      });
+      pipe(writable);
+    });
+
+    expect(loggedErrors.length).toBe(1);
+    expect(loggedErrors[0].message).toBe('Bye');
+    expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(
+      componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),
+    );
+  });
+
   it('should asynchronously load a lazy element', async () => {
     let resolveElement;
     const lazyElement = React.lazy(() => {
@@ -1797,7 +1843,7 @@ describe('ReactDOMFizzServer', () => {
   function normalizeCodeLocInfo(str) {
     return (
       str &&
-      String(str).replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
+      String(str).replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {
         return '\n    in ' + name + ' (at **)';
       })
     );

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -1105,8 +1105,8 @@ function pushComponentStack(task: Task): void {
 function createComponentStackFromType(
   parent: null | ComponentStackNode,
   type: Function | string | symbol,
-  owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
-  stack: null | Error, // DEV only
+  owner: void | null | ReactComponentInfo | ComponentStackNode, // DEV only
+  stack: void | null | string | Error, // DEV only
 ): ComponentStackNode {
   if (__DEV__) {
     return {
@@ -1122,6 +1122,20 @@ function createComponentStackFromType(
   };
 }
 
+function replaceSuspenseComponentStackWithSuspenseFallbackStack(
+  componentStack: null | ComponentStackNode,
+): null | ComponentStackNode {
+  if (componentStack === null) {
+    return null;
+  }
+  return createComponentStackFromType(
+    componentStack.parent,
+    'Suspense Fallback',
+    __DEV__ ? componentStack.owner : null,
+    __DEV__ ? componentStack.stack : null,
+  );
+}
+
 type ThrownInfo = {
   componentStack?: string,
 };
@@ -1350,6 +1364,8 @@ function renderSuspenseBoundary(
   contentRootSegment.parentFlushed = true;
 
   if (request.trackedPostpones !== null) {
+    // Stash the original stack frame.
+    const suspenseComponentStack = task.componentStack;
     // This is a prerender. In this mode we want to render the fallback synchronously and schedule
     // the content to render later. This is the opposite of what we do during a normal render
     // where we try to skip rendering the fallback if the content itself can render synchronously
@@ -1374,6 +1390,10 @@ function renderSuspenseBoundary(
       request.resumableState,
       prevContext,
     );
+    task.componentStack =
+      replaceSuspenseComponentStackWithSuspenseFallbackStack(
+        suspenseComponentStack,
+      );
     boundarySegment.status = RENDERING;
     try {
       renderNode(request, task, fallback, -1);
@@ -1419,7 +1439,7 @@ function renderSuspenseBoundary(
       task.context,
       task.treeContext,
       null, // The row gets reset inside the Suspense boundary.
-      task.componentStack,
+      suspenseComponentStack,
       !disableLegacyContext ? task.legacyContext : emptyContextObject,
       __DEV__ ? task.debugTask : null,
     );
@@ -1572,7 +1592,9 @@ function renderSuspenseBoundary(
       task.context,
       task.treeContext,
       task.row,
-      task.componentStack,
+      replaceSuspenseComponentStackWithSuspenseFallbackStack(
+        task.componentStack,
+      ),
       !disableLegacyContext ? task.legacyContext : emptyContextObject,
       __DEV__ ? task.debugTask : null,
     );
@@ -1744,7 +1766,7 @@ function replaySuspenseBoundary(
     task.context,
     task.treeContext,
     task.row,
-    task.componentStack,
+    replaceSuspenseComponentStackWithSuspenseFallbackStack(task.componentStack),
     !disableLegacyContext ? task.legacyContext : emptyContextObject,
     __DEV__ ? task.debugTask : null,
   );