[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
Sebastian "Sebbie" Silbermann committed
Jan 19, 2026 at 09:10 UTC
41b3e9a67004eb42631a9ff4504c56d22e2f97f0
4 files changed
+527
-27
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+188
-7
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108
);
109
}
110
111
+ /**
112
+ * Removes all stackframes not pointing into this file
113
+ */
114
+ function ignoreListStack(str) {
115
+ if (!str) {
116
+ return str;
117
+ }
118
+
119
+ let ignoreListedStack = '';
120
+ const lines = str.split('\n');
121
+
122
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
123
+ for (const line of lines) {
124
+ if (line.indexOf(__filename) === -1) {
125
+ } else {
126
+ ignoreListedStack += '\n' + line.replace(__dirname, '.');
127
+ }
128
+ }
129
+
130
+ return ignoreListedStack;
131
+ }
132
+
133
function readResult(stream) {
134
return new Promise((resolve, reject) => {
135
let buffer = '';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
806
}
807
});
808
809
+ // @gate enableHalt
810
+ it('includes source locations in component and owner stacks for halted Client components', async () => {
811
+ function SharedComponent({p1, p2, p3}) {
812
+ use(p1);
813
+ use(p2);
814
+ use(p3);
815
+ return <div>Hello, Dave!</div>;
816
+ }
817
+ const ClientComponentOnTheServer = clientExports(SharedComponent);
818
+ const ClientComponentOnTheClient = clientExports(
819
+ SharedComponent,
820
+ 123,
821
+ 'path/to/chunk.js',
822
+ );
823
+
824
+ let resolvePendingPromise;
825
+ function ServerComponent() {
826
+ const p1 = Promise.resolve();
827
+ const p2 = new Promise(resolve => {
828
+ resolvePendingPromise = value => {
829
+ p2.status = 'fulfilled';
830
+ p2.value = value;
831
+ resolve(value);
832
+ };
833
+ });
834
+ const p3 = new Promise(() => {});
835
+ return ReactServer.createElement(ClientComponentOnTheClient, {
836
+ p1: p1,
837
+ p2: p2,
838
+ p3: p3,
839
+ });
840
+ }
841
+
842
+ function App() {
843
+ return ReactServer.createElement(
844
+ 'html',
845
+ null,
846
+ ReactServer.createElement(
847
+ 'body',
848
+ null,
849
+ ReactServer.createElement(
850
+ ReactServer.Suspense,
851
+ {fallback: 'Loading...'},
852
+ ReactServer.createElement(ServerComponent, null),
853
+ ),
854
+ ),
855
+ );
856
+ }
857
+
858
+ const errors = [];
859
+ const rscStream = await serverAct(() =>
860
+ ReactServerDOMServer.renderToPipeableStream(
861
+ ReactServer.createElement(App, null),
862
+ webpackMap,
863
+ ),
864
+ );
865
+
866
+ const readable = new Stream.PassThrough(streamOptions);
867
+ rscStream.pipe(readable);
868
+
869
+ function ClientRoot({response}) {
870
+ return use(response);
871
+ }
872
+
873
+ const serverConsumerManifest = {
874
+ moduleMap: {
875
+ [webpackMap[ClientComponentOnTheClient.$$id].id]: {
876
+ '*': webpackMap[ClientComponentOnTheServer.$$id],
877
+ },
878
+ },
879
+ moduleLoading: webpackModuleLoading,
880
+ };
881
+
882
+ expect(errors).toEqual([]);
883
+
884
+ function ClientRoot({response}) {
885
+ return use(response);
886
+ }
887
+
888
+ const response = ReactServerDOMClient.createFromNodeStream(
889
+ readable,
890
+ serverConsumerManifest,
891
+ );
892
+
893
+ let componentStack;
894
+ let ownerStack;
895
+
896
+ const clientAbortController = new AbortController();
897
+
898
+ const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
899
+ React.createElement(ClientRoot, {response}),
900
+ {
901
+ signal: clientAbortController.signal,
902
+ onError(error, errorInfo) {
903
+ componentStack = errorInfo.componentStack;
904
+ ownerStack = React.captureOwnerStack
905
+ ? React.captureOwnerStack()
906
+ : null;
907
+ },
908
+ },
909
+ );
910
+
911
+ resolvePendingPromise('custom-instrum-resolve');
912
+ await serverAct(
913
+ async () =>
914
+ new Promise(resolve => {
915
+ setImmediate(() => {
916
+ clientAbortController.abort();
917
+ resolve();
918
+ });
919
+ }),
920
+ );
921
+
922
+ const fizzPrerenderStream = await fizzPrerenderStreamResult;
923
+ const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
924
+
925
+ expect(prerenderHTML).toContain('Loading...');
926
+
927
+ if (__DEV__) {
928
+ expect(normalizeCodeLocInfo(componentStack)).toBe(
929
+ '\n' +
930
+ ' in SharedComponent (at **)\n' +
931
+ ' in ServerComponent' +
932
+ (gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
933
+ '\n' +
934
+ ' in Suspense\n' +
935
+ ' in body\n' +
936
+ ' in html\n' +
937
+ ' in App (at **)\n' +
938
+ ' in ClientRoot (at **)',
939
+ );
940
+ } else {
941
+ expect(normalizeCodeLocInfo(componentStack)).toBe(
942
+ '\n' +
943
+ ' in SharedComponent (at **)\n' +
944
+ ' in Suspense\n' +
945
+ ' in body\n' +
946
+ ' in html\n' +
947
+ ' in ClientRoot (at **)',
948
+ );
949
+ }
950
+
951
+ if (__DEV__) {
952
+ expect(ignoreListStack(ownerStack)).toBe(
953
+ // eslint-disable-next-line react-internal/safe-string-coercion
954
+ '' +
955
+ // The concrete location may change as this test is updated.
956
+ // Just make sure they still point at React.use(p2)
957
+ (gate(flags => flags.enableAsyncDebugInfo)
958
+ ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959
+ : '') +
960
+ '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)' +
961
+ '\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962
+ );
963
+ } else {
964
+ expect(ownerStack).toBeNull();
965
+ }
966
+ });
967
+
968
// @gate enableHalt
969
it('includes deeper location for aborted stacks', async () => {
970
async function getData() {
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
1545
'\n' +
1546
' in Dynamic' +
1547
(gate(flags => flags.enableAsyncDebugInfo)
1367
- ? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548
+ ? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
1549
: '\n') +
1550
' in body\n' +
1551
' in html\n' +
1371
- ' in App (file://ReactFlightDOMNode-test.js:1251:25)\n' +
1372
- ' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552
+ ' in App (file://ReactFlightDOMNode-test.js:1432:25)\n' +
1553
+ ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
1554
);
1555
} else {
1556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
1559
'\n' +
1560
' in body\n' +
1561
' in html\n' +
1381
- ' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562
+ ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
1563
);
1564
}
1565
@@ -1388,8 +1569,8 @@ describe('ReactFlightDOMNode', () => {
1569
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1570
).toBe(
1571
'\n' +
1391
- ' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n' +
1392
- ' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572
+ ' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n' +
1573
+ ' in App (file://ReactFlightDOMNode-test.js:1432:25)',
1574
);
1575
} else {
1576
expect(
@@ -1397,7 +1578,7 @@ describe('ReactFlightDOMNode', () => {
1578
).toBe(
1579
'' +
1580
'\n' +
1400
- ' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581
+ ' in App (file://ReactFlightDOMNode-test.js:1432:25)',
1582
);
1583
}
1584
} else {
packages/react-server/src/ReactFizzServer.js
+100
-7
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190
import noop from 'shared/noop';
191
import getComponentNameFromType from 'shared/getComponentNameFromType';
192
import isArray from 'shared/isArray';
193
-import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
193
+import {
194
+ SuspenseException,
195
+ getSuspendedThenable,
196
+ ensureSuspendableThenableStateDEV,
197
+ getSuspendedCallSiteStackDEV,
198
+ getSuspendedCallSiteDebugTaskDEV,
199
+ setCaptureSuspendedCallSiteDEV,
200
+} from './ReactFizzThenable';
201
202
// Linked list representing the identity of a component given the component/tag name and key.
203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
362
const ABORTING = 12;
363
const CLOSING = 13;
364
const CLOSED = 14;
365
+const STALLED_DEV = 15;
366
367
export opaque type Request = {
368
destination: null | Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
371
+renderState: RenderState,
372
+rootFormatContext: FormatContext,
373
+progressiveChunkSize: number,
366
- status: 10 | 11 | 12 | 13 | 14,
374
+ status: 10 | 11 | 12 | 13 | 14 | 15,
375
fatalError: mixed,
376
nextSegmentId: number,
377
allPendingTasks: number, // when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
1031
}
1032
}
1033
1034
+// performWork + retryTask without mutation
1035
+function rerenderStalledTask(request: Request, task: Task): void {
1036
+ const prevStatus = request.status;
1037
+ request.status = STALLED_DEV;
1038
+
1039
+ const prevContext = getActiveContext();
1040
+ const prevDispatcher = ReactSharedInternals.H;
1041
+ ReactSharedInternals.H = HooksDispatcher;
1042
+ const prevAsyncDispatcher = ReactSharedInternals.A;
1043
+ ReactSharedInternals.A = DefaultAsyncDispatcher;
1044
+
1045
+ const prevRequest = currentRequest;
1046
+ currentRequest = request;
1047
+
1048
+ const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
1049
+ ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
1050
+
1051
+ const prevResumableState = currentResumableState;
1052
+ setCurrentResumableState(request.resumableState);
1053
+ switchContext(task.context);
1054
+ const prevTaskInDEV = currentTaskInDEV;
1055
+ setCurrentTaskInDEV(task);
1056
+ try {
1057
+ retryNode(request, task);
1058
+ } catch (x) {
1059
+ // Suspended again.
1060
+ resetHooksState();
1061
+ } finally {
1062
+ setCurrentTaskInDEV(prevTaskInDEV);
1063
+ setCurrentResumableState(prevResumableState);
1064
+
1065
+ ReactSharedInternals.H = prevDispatcher;
1066
+ ReactSharedInternals.A = prevAsyncDispatcher;
1067
+
1068
+ ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
1069
+ if (prevDispatcher === HooksDispatcher) {
1070
+ // This means that we were in a reentrant work loop. This could happen
1071
+ // in a renderer that supports synchronous work like renderToString,
1072
+ // when it's called from within another renderer.
1073
+ // Normally we don't bother switching the contexts to their root/default
1074
+ // values when leaving because we'll likely need the same or similar
1075
+ // context again. However, when we're inside a synchronous loop like this
1076
+ // we'll to restore the context to what it was before returning.
1077
+ switchContext(prevContext);
1078
+ }
1079
+ currentRequest = prevRequest;
1080
+ request.status = prevStatus;
1081
+ }
1082
+}
1083
+
1084
+function pushSuspendedCallSiteOnComponentStack(
1085
+ request: Request,
1086
+ task: Task,
1087
+): void {
1088
+ setCaptureSuspendedCallSiteDEV(true);
1089
+ const restoreThenableState = ensureSuspendableThenableStateDEV(
1090
+ // refined at the callsite
1091
+ ((task.thenableState: any): ThenableState),
1092
+ );
1093
+ try {
1094
+ rerenderStalledTask(request, task);
1095
+ } finally {
1096
+ restoreThenableState();
1097
+ setCaptureSuspendedCallSiteDEV(false);
1098
+ }
1099
+
1100
+ const suspendCallSiteStack = getSuspendedCallSiteStackDEV();
1101
+ const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();
1102
+
1103
+ if (suspendCallSiteStack !== null) {
1104
+ const ownerStack = task.componentStack;
1105
+ task.componentStack = {
1106
+ // The owner of the suspended call site would be the owner of this task.
1107
+ // We need the task itself otherwise we'd miss a frame.
1108
+ owner: ownerStack,
1109
+ parent: suspendCallSiteStack.parent,
1110
+ stack: suspendCallSiteStack.stack,
1111
+ type: suspendCallSiteStack.type,
1112
+ };
1113
+ }
1114
+ task.debugTask = suspendCallSiteDebugTask;
1115
+}
1116
+
1117
function pushServerComponentStack(
1118
task: Task,
1119
debugInfo: void | null | ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
2814
const init = lazyComponent._init;
2815
Component = init(payload);
2816
}
2726
- if (request.status === ABORTING) {
2817
+ if (
2818
+ request.status === ABORTING &&
2819
+ // We're going to discard this render anyway.
2820
+ // We just need to reach the point where we suspended in dev.
2821
+ (!__DEV__ || request.status !== STALLED_DEV)
2822
+ ) {
2823
// eslint-disable-next-line no-throw-literal
2824
throw null;
2825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4631
debugInfo = node._debugInfo;
4632
}
4633
pushHaltedAwaitOnComponentStack(task, debugInfo);
4538
- /*
4634
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.
4635
+ pushSuspendedCallSiteOnComponentStack(request, task);
4636
}
4543
- */
4637
}
4638
}
4639
packages/react-server/src/ReactFizzThenable.js
+133
-4
@@ -7,20 +7,19 @@
7
* @flow
8
*/
9
10
-// Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally,
10
+// Corresponds to ReactFiberThenable and ReactFlightThenable modules. Generally,
11
// changes to one module should be reflected in the others.
12
13
-// TODO: Rename this module and the corresponding Fiber one to "Thenable"
14
-// instead of "Wakeable". Or some other more appropriate name.
15
-
13
import type {
14
Thenable,
15
PendingThenable,
16
FulfilledThenable,
17
RejectedThenable,
18
} from 'shared/ReactTypes';
19
+import type {ComponentStackNode} from './ReactFizzComponentStack';
20
21
import noop from 'shared/noop';
22
+import {currentTaskInDEV} from './ReactFizzCurrentTask';
23
24
export opaque type ThenableState = Array<Thenable<any>>;
25
@@ -126,6 +125,9 @@ export function trackUsedThenable<T>(
125
// get captured by the work loop, log a warning, because that means
126
// something in userspace must have caught it.
127
suspendedThenable = thenable;
128
+ if (__DEV__ && shouldCaptureSuspendedCallSite) {
129
+ captureSuspendedCallSite();
130
+ }
131
throw SuspenseException;
132
}
133
}
@@ -163,3 +165,130 @@ export function getSuspendedThenable(): Thenable<mixed> {
165
suspendedThenable = null;
166
return thenable;
167
}
168
+
169
+let shouldCaptureSuspendedCallSite: boolean = false;
170
+export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
171
+ if (!__DEV__) {
172
+ // eslint-disable-next-line react-internal/prod-error-codes
173
+ throw new Error(
174
+ 'setCaptureSuspendedCallSiteDEV was called in a production environment. ' +
175
+ 'This is a bug in React.',
176
+ );
177
+ }
178
+ shouldCaptureSuspendedCallSite = capture;
179
+}
180
+
181
+// DEV-only
182
+let suspendedCallSiteStack: ComponentStackNode | null = null;
183
+let suspendedCallSiteDebugTask: ConsoleTask | null = null;
184
+function captureSuspendedCallSite(): void {
185
+ // This is currently only used when aborting in Fizz.
186
+ // You can only abort the render in Fizz and Flight.
187
+ // In Fiber we only track suspended use via DevTools.
188
+ // In Flight, we track suspended use via async debug info.
189
+ const currentTask = currentTaskInDEV;
190
+ if (currentTask === null) {
191
+ // eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
192
+ throw new Error(
193
+ 'Expected to have a current task when tracking a suspend call site. ' +
194
+ 'This is a bug in React.',
195
+ );
196
+ }
197
+ const currentComponentStack = currentTask.componentStack;
198
+ if (currentComponentStack === null) {
199
+ // eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
200
+ throw new Error(
201
+ 'Expected to have a component stack on the current task when ' +
202
+ 'tracking a suspended call site. This is a bug in React.',
203
+ );
204
+ }
205
+ suspendedCallSiteStack = {
206
+ parent: currentComponentStack.parent,
207
+ type: currentComponentStack.type,
208
+ owner: currentComponentStack.owner,
209
+ stack: Error('react-stack-top-frame'),
210
+ };
211
+ // TODO: If this is used in error handlers, the ConsoleTask stack
212
+ // will just be this debugTask + the stack of the abort() call which usually means
213
+ // it's just this debugTask.
214
+ // Ideally we'd be able to reconstruct the owner ConsoleTask as well.
215
+ // The stack of the debugTask would not point to the suspend location anyway.
216
+ // The focus is really on callsite which should be used in captureOwnerStack().
217
+ suspendedCallSiteDebugTask = currentTask.debugTask;
218
+}
219
+export function getSuspendedCallSiteStackDEV(): ComponentStackNode | null {
220
+ if (__DEV__) {
221
+ if (suspendedCallSiteStack === null) {
222
+ return null;
223
+ }
224
+ const callSite = suspendedCallSiteStack;
225
+ suspendedCallSiteStack = null;
226
+ return callSite;
227
+ } else {
228
+ // eslint-disable-next-line react-internal/prod-error-codes
229
+ throw new Error(
230
+ 'getSuspendedCallSiteDEV was called in a production environment. ' +
231
+ 'This is a bug in React.',
232
+ );
233
+ }
234
+}
235
+
236
+export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
237
+ if (__DEV__) {
238
+ if (suspendedCallSiteDebugTask === null) {
239
+ return null;
240
+ }
241
+ const debugTask = suspendedCallSiteDebugTask;
242
+ suspendedCallSiteDebugTask = null;
243
+ return debugTask;
244
+ } else {
245
+ // eslint-disable-next-line react-internal/prod-error-codes
246
+ throw new Error(
247
+ 'getSuspendedCallSiteDebugTaskDEV was called in a production environment. ' +
248
+ 'This is a bug in React.',
249
+ );
250
+ }
251
+}
252
+
253
+export function ensureSuspendableThenableStateDEV(
254
+ thenableState: ThenableState,
255
+): () => void {
256
+ if (__DEV__) {
257
+ const lastThenable = thenableState[thenableState.length - 1];
258
+ // Reset the last thenable back to pending.
259
+ switch (lastThenable.status) {
260
+ case 'fulfilled':
261
+ const previousThenableValue = lastThenable.value;
262
+ // $FlowIgnore[method-unbinding] We rebind .then immediately.
263
+ const previousThenableThen = lastThenable.then.bind(lastThenable);
264
+ delete lastThenable.value;
265
+ delete (lastThenable: any).status;
266
+ // We'll call .then again if we resuspend. Since we potentially corrupted
267
+ // the internal state of unknown classes, we need to diffuse the potential
268
+ // crash by replacing the .then method with a noop.
269
+ // $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
270
+ lastThenable.then = noop;
271
+ return () => {
272
+ // $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
273
+ lastThenable.then = previousThenableThen;
274
+ lastThenable.value = previousThenableValue;
275
+ lastThenable.status = 'fulfilled';
276
+ };
277
+ case 'rejected':
278
+ const previousThenableReason = lastThenable.reason;
279
+ delete lastThenable.reason;
280
+ delete (lastThenable: any).status;
281
+ return () => {
282
+ lastThenable.reason = previousThenableReason;
283
+ lastThenable.status = 'rejected';
284
+ };
285
+ }
286
+ return noop;
287
+ } else {
288
+ // eslint-disable-next-line react-internal/prod-error-codes
289
+ throw new Error(
290
+ 'ensureSuspendableThenableStateDEV was called in a production environment. ' +
291
+ 'This is a bug in React.',
292
+ );
293
+ }
294
+}
packages/react-server/src/__tests__/ReactServer-test.js
+106
-9
@@ -9,6 +9,7 @@
9
*/
10
11
'use strict';
12
+import {AsyncLocalStorage} from 'node:async_hooks';
13
14
let act;
15
let React;
@@ -27,10 +28,43 @@ function normalizeCodeLocInfo(str) {
28
);
29
}
30
31
+/**
32
+ * Removes all stackframes not pointing into this file
33
+ */
34
+function ignoreListStack(str) {
35
+ if (!str) {
36
+ return str;
37
+ }
38
+
39
+ let ignoreListedStack = '';
40
+ const lines = str.split('\n');
41
+
42
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
43
+ for (const line of lines) {
44
+ if (line.indexOf(__filename) === -1) {
45
+ } else {
46
+ ignoreListedStack += '\n' + line.replace(__dirname, '.');
47
+ }
48
+ }
49
+
50
+ return ignoreListedStack;
51
+}
52
+
53
+const currentTask = new AsyncLocalStorage({defaultValue: null});
54
+
55
describe('ReactServer', () => {
56
beforeEach(() => {
57
jest.resetModules();
58
59
+ console.createTask = jest.fn(taskName => {
60
+ return {
61
+ run: taskFn => {
62
+ const parentTask = currentTask.getStore() || '';
63
+ return currentTask.run(parentTask + '\n' + taskName, taskFn);
64
+ },
65
+ };
66
+ });
67
+
68
act = require('internal-test-utils').act;
69
React = require('react');
70
ReactNoopServer = require('react-noop-renderer/server');
@@ -49,29 +83,67 @@ describe('ReactServer', () => {
83
});
84
85
it('has Owner Stacks in DEV when aborted', async () => {
52
- function Component({promise}) {
53
- React.use(promise);
86
+ const Context = React.createContext(null);
87
+
88
+ function Component({p1, p2, p3}) {
89
+ const context = React.use(Context);
90
+ if (context === null) {
91
+ throw new Error('Missing context');
92
+ }
93
+ React.use(p1);
94
+ React.use(p2);
95
+ React.use(p3);
96
return <div>Hello, Dave!</div>;
97
}
56
- function App({promise}) {
57
- return <Component promise={promise} />;
98
+ function Indirection({p1, p2, p3}) {
99
+ return (
100
+ <div>
101
+ <Component p1={p1} p2={p2} p3={p3} />
102
+ </div>
103
+ );
104
+ }
105
+ function App({p1, p2, p3}) {
106
+ return (
107
+ <section>
108
+ <div>
109
+ <Indirection p1={p1} p2={p2} p3={p3} />
110
+ </div>
111
+ </section>
112
+ );
113
}
114
115
let caughtError;
116
let componentStack;
117
let ownerStack;
118
+ let task;
119
+ const resolvedPromise = Promise.resolve('one');
120
+ resolvedPromise.status = 'fulfilled';
121
+ resolvedPromise.value = 'one';
122
+ let resolvePendingPromise;
123
+ const pendingPromise = new Promise(resolve => {
124
+ resolvePendingPromise = value => {
125
+ pendingPromise.status = 'fulfilled';
126
+ pendingPromise.value = value;
127
+ resolve(value);
128
+ };
129
+ });
130
+ const hangingPromise = new Promise(() => {});
131
const result = ReactNoopServer.render(
64
- <App promise={new Promise(() => {})} />,
132
+ <Context value="provided">
133
+ <App p1={resolvedPromise} p2={pendingPromise} p3={hangingPromise} />
134
+ </Context>,
135
{
136
onError: (error, errorInfo) => {
137
caughtError = error;
138
componentStack = errorInfo.componentStack;
139
ownerStack = __DEV__ ? React.captureOwnerStack() : null;
140
+ task = currentTask.getStore();
141
},
142
},
143
);
144
145
await act(async () => {
146
+ resolvePendingPromise('two');
147
result.abort();
148
});
149
expect(caughtError).toEqual(
@@ -80,10 +152,35 @@ describe('ReactServer', () => {
152
}),
153
);
154
expect(normalizeCodeLocInfo(componentStack)).toEqual(
83
- '\n in Component (at **)' + '\n in App (at **)',
84
- );
85
- expect(normalizeCodeLocInfo(ownerStack)).toEqual(
86
- __DEV__ ? '\n in App (at **)' : null,
155
+ '\n in Component (at **)' +
156
+ '\n in div' +
157
+ '\n in Indirection (at **)' +
158
+ '\n in div' +
159
+ '\n in section' +
160
+ '\n in App (at **)',
161
);
162
+ if (__DEV__) {
163
+ // The concrete location may change as this test is updated.
164
+ // Just make sure they still point at the same code
165
+ if (gate(flags => flags.enableAsyncDebugInfo)) {
166
+ expect(ignoreListStack(ownerStack)).toEqual(
167
+ '' +
168
+ // Pointing at React.use(p2)
169
+ '\n at Component (./ReactServer-test.js:94:13)' +
170
+ '\n at Indirection (./ReactServer-test.js:101:44)' +
171
+ '\n at App (./ReactServer-test.js:109:46)',
172
+ );
173
+ } else {
174
+ expect(ignoreListStack(ownerStack)).toEqual(
175
+ '' +
176
+ '\n at Indirection (./ReactServer-test.js:101:44)' +
177
+ '\n at App (./ReactServer-test.js:109:46)',
178
+ );
179
+ }
180
+ expect(task).toEqual('\n<Component>');
181
+ } else {
182
+ expect(ownerStack).toBeNull();
183
+ expect(task).toEqual(undefined);
184
+ }
185
});
186
});