Skip to content
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
48 changes: 47 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 **)';
})
);
Expand Down
32 changes: 27 additions & 5 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -1122,6 +1122,20 @@ function createComponentStackFromType(
};
}

function replaceSuspenseComponentStackWithSuspenseFallbackStack(
componentStack: null | ComponentStackNode,
): null | ComponentStackNode {
if (componentStack === null) {
return null;
}
return createComponentStackFromType(
componentStack.parent,
'Suspense Fallback',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can't get the actual type from props because it could be a lazy node?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that the real type is modeled as a symbol. However, we don't have a symbol for the fallback case. So I just gave it a string name since in practice we format them the same as built-ins.

__DEV__ ? componentStack.owner : null,
__DEV__ ? componentStack.stack : null,
);
}

type ThrownInfo = {
componentStack?: string,
};
Expand Down Expand Up @@ -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
Expand All @@ -1374,6 +1390,10 @@ function renderSuspenseBoundary(
request.resumableState,
prevContext,
);
task.componentStack =
replaceSuspenseComponentStackWithSuspenseFallbackStack(
suspenseComponentStack,
);
boundarySegment.status = RENDERING;
try {
renderNode(request, task, fallback, -1);
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -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,
);
Expand Down
Loading