Skip to content

Commit 1dd02bd

Browse files
committed
[Fizz] Push a stalled use() to the ownerStack/debugTask
1 parent 8ac5f4e commit 1dd02bd

File tree

6 files changed

+153
-8
lines changed

6 files changed

+153
-8
lines changed

fixtures/fizz/server/render-to-stream.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ module.exports = function render(url, res) {
4545
onError(x) {
4646
didError = true;
4747
console.error(x);
48+
// Redundant with `console.createTask`. Only added for debugging.
49+
console.error(
50+
'The above error occurred during server rendering: %s',
51+
React.captureOwnerStack()
52+
);
4853
},
4954
});
5055
// Abandon and switch to client rendering if enough time passes.

fixtures/fizz/src/App.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66
*
77
*/
88

9+
import {Suspense} from 'react';
910
import Html from './Html';
1011
import BigComponent from './BigComponent';
12+
import MaybeHaltedComponent from './MaybeHaltedComponent';
1113

12-
export default function App({assets, title}) {
14+
const serverHalt =
15+
typeof window === 'undefined'
16+
? new Promise(() => {})
17+
: Promise.resolve('client');
18+
19+
export default function App({assets, promise, title}) {
1320
const components = [];
1421

1522
for (let i = 0; i <= 250; i++) {
@@ -21,6 +28,10 @@ export default function App({assets, title}) {
2128
<h1>{title}</h1>
2229
{components}
2330
<h1>all done</h1>
31+
<h2>or maybe not</h2>
32+
<Suspense fallback="loading more...">
33+
<MaybeHaltedComponent promise={serverHalt} />
34+
</Suspense>
2435
</Html>
2536
);
2637
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {use} from 'react';
2+
3+
export default function MaybeHaltedComponent({promise}) {
4+
use(promise);
5+
return <div>Did not halt</div>;
6+
}

packages/react-server/src/ReactFizzServer.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,13 @@ import assign from 'shared/assign';
190190
import noop from 'shared/noop';
191191
import getComponentNameFromType from 'shared/getComponentNameFromType';
192192
import isArray from 'shared/isArray';
193-
import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
193+
import {
194+
SuspenseException,
195+
getSuspendedThenable,
196+
getSuspendedCallSiteStackDEV,
197+
getSuspendedCallSiteDebugTaskDEV,
198+
setCaptureSuspendedCallSiteDEV,
199+
} from './ReactFizzThenable';
194200

195201
// Linked list representing the identity of a component given the component/tag name and key.
196202
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -1023,6 +1029,37 @@ function pushHaltedAwaitOnComponentStack(
10231029
}
10241030
}
10251031

1032+
function pushSuspendedCallSiteOnComponentStack(
1033+
request: Request,
1034+
task: Task,
1035+
): void {
1036+
setCaptureSuspendedCallSiteDEV(true);
1037+
let suspendCallSiteStack: ComponentStackNode | null = null;
1038+
let suspendCallSiteDebugTask: ConsoleTask | null = null;
1039+
const previousPingedTasks = request.pingedTasks;
1040+
try {
1041+
// TODO: Use a dedicated method to re-render instead of abusing ping.
1042+
request.pingedTasks = [task];
1043+
performWork(request);
1044+
suspendCallSiteStack = getSuspendedCallSiteStackDEV();
1045+
suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();
1046+
} finally {
1047+
request.pingedTasks = previousPingedTasks;
1048+
setCaptureSuspendedCallSiteDEV(false);
1049+
}
1050+
1051+
if (suspendCallSiteStack !== null) {
1052+
const ownerStack = task.componentStack;
1053+
task.componentStack = {
1054+
owner: ownerStack,
1055+
parent: suspendCallSiteStack.parent,
1056+
stack: suspendCallSiteStack.stack,
1057+
type: suspendCallSiteStack.type,
1058+
};
1059+
}
1060+
task.debugTask = suspendCallSiteDebugTask;
1061+
}
1062+
10261063
function pushServerComponentStack(
10271064
task: Task,
10281065
debugInfo: void | null | ReactDebugInfo,
@@ -4535,12 +4572,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354572
debugInfo = node._debugInfo;
45364573
}
45374574
pushHaltedAwaitOnComponentStack(task, debugInfo);
4538-
/*
45394575
if (task.thenableState !== null) {
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4576+
pushSuspendedCallSiteOnComponentStack(request, task);
45424577
}
4543-
*/
45444578
}
45454579
}
45464580

@@ -4962,7 +4996,9 @@ function retryRenderTask(
49624996
task: RenderTask,
49634997
segment: Segment,
49644998
): void {
4965-
if (segment.status !== PENDING) {
4999+
// TODO: We only retry when aborted to get the suspended callsite.
5000+
// Use a dedicated mechanism to re-render.
5001+
if (segment.status !== PENDING && segment.status !== ABORTED) {
49665002
// We completed this by other means before we had a chance to retry it.
49675003
return;
49685004
}

packages/react-server/src/ReactFizzThenable.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import type {
1919
FulfilledThenable,
2020
RejectedThenable,
2121
} from 'shared/ReactTypes';
22+
import type {ComponentStackNode} from './ReactFizzComponentStack';
2223

2324
import noop from 'shared/noop';
25+
import {currentTaskInDEV} from './ReactFizzCurrentTask';
2426

2527
export opaque type ThenableState = Array<Thenable<any>>;
2628

@@ -126,6 +128,9 @@ export function trackUsedThenable<T>(
126128
// get captured by the work loop, log a warning, because that means
127129
// something in userspace must have caught it.
128130
suspendedThenable = thenable;
131+
if (__DEV__ && shouldCaptureSuspendedCallSite) {
132+
captureSuspendedCallSite();
133+
}
129134
throw SuspenseException;
130135
}
131136
}
@@ -163,3 +168,77 @@ export function getSuspendedThenable(): Thenable<mixed> {
163168
suspendedThenable = null;
164169
return thenable;
165170
}
171+
172+
let shouldCaptureSuspendedCallSite: boolean = false;
173+
export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
174+
if (!__DEV__) {
175+
// eslint-disable-next-line react-internal/prod-error-codes
176+
throw new Error(
177+
'setCaptureSuspendedCallSiteDEV was called in a production environment. ' +
178+
'This is a bug in React.',
179+
);
180+
}
181+
shouldCaptureSuspendedCallSite = capture;
182+
}
183+
184+
// DEV-only
185+
let suspendedCallSiteStack: ComponentStackNode | null = null;
186+
let suspendedCallSiteDebugTask: ConsoleTask | null = null;
187+
function captureSuspendedCallSite(): void {
188+
const currentTask = currentTaskInDEV;
189+
if (currentTask === null) {
190+
// eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
191+
throw new Error(
192+
'Expected to have a current task when tracking a suspend call site. ' +
193+
'This is a bug in React.',
194+
);
195+
}
196+
const currentComponentStack = currentTask.componentStack;
197+
if (currentComponentStack === null) {
198+
// eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
199+
throw new Error(
200+
'Expected to have a component stack on the current task when ' +
201+
'tracking a suspended call site. This is a bug in React.',
202+
);
203+
}
204+
suspendedCallSiteStack = {
205+
parent: currentComponentStack.parent,
206+
type: currentComponentStack.type,
207+
owner: currentComponentStack.owner,
208+
stack: Error('react-stack-top-frame'),
209+
};
210+
suspendedCallSiteDebugTask = currentTask.debugTask;
211+
}
212+
export function getSuspendedCallSiteStackDEV(): ComponentStackNode | null {
213+
if (__DEV__) {
214+
if (suspendedCallSiteStack === null) {
215+
return null;
216+
}
217+
const callSite = suspendedCallSiteStack;
218+
suspendedCallSiteStack = null;
219+
return callSite;
220+
} else {
221+
// eslint-disable-next-line react-internal/prod-error-codes
222+
throw new Error(
223+
'getSuspendedCallSiteDEV was called in a production environment. ' +
224+
'This is a bug in React.',
225+
);
226+
}
227+
}
228+
229+
export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
230+
if (__DEV__) {
231+
if (suspendedCallSiteDebugTask === null) {
232+
return null;
233+
}
234+
const debugTask = suspendedCallSiteDebugTask;
235+
suspendedCallSiteDebugTask = null;
236+
return debugTask;
237+
} else {
238+
// eslint-disable-next-line react-internal/prod-error-codes
239+
throw new Error(
240+
'getSuspendedCallSiteDebugTaskDEV was called in a production environment. ' +
241+
'This is a bug in React.',
242+
);
243+
}
244+
}

packages/react-server/src/__tests__/ReactServer-test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,15 @@ describe('ReactServer', () => {
8383
'\n in Component (at **)' + '\n in App (at **)',
8484
);
8585
expect(normalizeCodeLocInfo(ownerStack)).toEqual(
86-
__DEV__ ? '\n in App (at **)' : null,
86+
__DEV__
87+
? '' +
88+
'\n in trackUsedThenable (at **)' +
89+
'\n in unwrapThenable (at **)' +
90+
'\n in use (at **)' +
91+
'\n in use (at **)' +
92+
'\n in Component (at **)' +
93+
'\n in App (at **)'
94+
: null,
8795
);
8896
});
8997
});

0 commit comments

Comments
 (0)