Skip to content

Commit 0a9111f

Browse files
committed
Wire up owner stacks in Flight
This exposes it to captureOwnerStack(). In this case we install it permanently as we only allow one RSC renderer which then supports async contexts. It also exposes it to component stack addendums that React adds to its own console.errors. At least for now.
1 parent cfb8945 commit 0a9111f

File tree

4 files changed

+155
-3
lines changed

4 files changed

+155
-3
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

+33-3
Original file line numberDiff line numberDiff line change
@@ -1441,9 +1441,7 @@ describe('ReactFlight', () => {
14411441
<div>{Array(6).fill(<NoKey />)}</div>,
14421442
);
14431443
ReactNoopFlightClient.read(transport);
1444-
}).toErrorDev('Each child in a list should have a unique "key" prop.', {
1445-
withoutStack: gate(flags => flags.enableOwnerStacks),
1446-
});
1444+
}).toErrorDev('Each child in a list should have a unique "key" prop.');
14471445
});
14481446

14491447
it('should warn in DEV a child is missing keys in client component', async () => {
@@ -2728,4 +2726,36 @@ describe('ReactFlight', () => {
27282726

27292727
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb</span>);
27302728
});
2729+
2730+
// @gate __DEV__ && enableOwnerStacks
2731+
it('can get the component owner stacks during rendering in dev', () => {
2732+
let stack;
2733+
2734+
function Foo() {
2735+
return ReactServer.createElement(Bar, null);
2736+
}
2737+
function Bar() {
2738+
return ReactServer.createElement(
2739+
'div',
2740+
null,
2741+
ReactServer.createElement(Baz, null),
2742+
);
2743+
}
2744+
2745+
function Baz() {
2746+
stack = ReactServer.captureOwnerStack();
2747+
return ReactServer.createElement('span', null, 'hi');
2748+
}
2749+
ReactNoopFlightServer.render(
2750+
ReactServer.createElement(
2751+
'div',
2752+
null,
2753+
ReactServer.createElement(Foo, null),
2754+
),
2755+
);
2756+
2757+
expect(normalizeCodeLocInfo(stack)).toBe(
2758+
'\n in Bar (at **)' + '\n in Foo (at **)',
2759+
);
2760+
});
27312761
});

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

+47
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ let ReactServerDOMServer;
3939
let ReactServerDOMClient;
4040
let use;
4141

42+
function normalizeCodeLocInfo(str) {
43+
return (
44+
str &&
45+
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
46+
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
47+
})
48+
);
49+
}
50+
4251
describe('ReactFlightDOMEdge', () => {
4352
beforeEach(() => {
4453
jest.resetModules();
@@ -883,4 +892,42 @@ describe('ReactFlightDOMEdge', () => {
883892
);
884893
}
885894
});
895+
896+
// @gate __DEV__ && enableOwnerStacks
897+
it('can get the component owner stacks asynchronously', async () => {
898+
let stack;
899+
900+
function Foo() {
901+
return ReactServer.createElement(Bar, null);
902+
}
903+
function Bar() {
904+
return ReactServer.createElement(
905+
'div',
906+
null,
907+
ReactServer.createElement(Baz, null),
908+
);
909+
}
910+
911+
const promise = Promise.resolve(0);
912+
913+
async function Baz() {
914+
await promise;
915+
stack = ReactServer.captureOwnerStack();
916+
return ReactServer.createElement('span', null, 'hi');
917+
}
918+
919+
const stream = ReactServerDOMServer.renderToReadableStream(
920+
ReactServer.createElement(
921+
'div',
922+
null,
923+
ReactServer.createElement(Foo, null),
924+
),
925+
webpackMap,
926+
);
927+
await readResult(stream);
928+
929+
expect(normalizeCodeLocInfo(stack)).toBe(
930+
'\n in Bar (at **)' + '\n in Foo (at **)',
931+
);
932+
});
886933
});

packages/react-server/src/ReactFlightServer.js

+23
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher';
9797

9898
import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner';
9999

100+
import {getOwnerStackByComponentInfoInDev} from './flight/ReactFlightComponentStack';
101+
100102
import {
101103
getIteratorFn,
102104
REACT_ELEMENT_TYPE,
@@ -317,6 +319,21 @@ if (
317319
patchConsole(console, 'warn');
318320
}
319321

322+
function getCurrentStackInDEV(): string {
323+
if (__DEV__) {
324+
if (enableOwnerStacks) {
325+
const owner: null | ReactComponentInfo = resolveOwner();
326+
if (owner === null) {
327+
return '';
328+
}
329+
return getOwnerStackByComponentInfoInDev(owner);
330+
}
331+
// We don't have Parent Stacks in Flight.
332+
return '';
333+
}
334+
return '';
335+
}
336+
320337
const ObjectPrototype = Object.prototype;
321338

322339
type JSONValue =
@@ -491,6 +508,12 @@ function RequestInstance(
491508
);
492509
}
493510
ReactSharedInternals.A = DefaultAsyncDispatcher;
511+
if (__DEV__) {
512+
// Unlike Fizz or Fiber, we don't reset this and just keep it on permanently.
513+
// This lets it act more like the AsyncDispatcher so that we can get the
514+
// stack asynchronously too.
515+
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
516+
}
494517

495518
const abortSet: Set<Task> = new Set();
496519
const pingedTasks: Array<Task> = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactComponentInfo} from 'shared/ReactTypes';
11+
12+
import {describeBuiltInComponentFrame} from 'shared/ReactComponentStackFrame';
13+
14+
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
15+
16+
export function getOwnerStackByComponentInfoInDev(
17+
componentInfo: ReactComponentInfo,
18+
): string {
19+
if (!enableOwnerStacks || !__DEV__) {
20+
return '';
21+
}
22+
try {
23+
let info = '';
24+
25+
// The owner stack of the current component will be where it was created, i.e. inside its owner.
26+
// There's no actual name of the currently executing component. Instead, that is available
27+
// on the regular stack that's currently executing. However, if there is no owner at all, then
28+
// there's no stack frame so we add the name of the root component to the stack to know which
29+
// component is currently executing.
30+
if (!componentInfo.owner && typeof componentInfo.name === 'string') {
31+
return describeBuiltInComponentFrame(componentInfo.name);
32+
}
33+
34+
let owner: void | null | ReactComponentInfo = componentInfo;
35+
36+
while (owner) {
37+
if (typeof owner.stack === 'string') {
38+
// Server Component
39+
const ownerStack: string = owner.stack;
40+
owner = owner.owner;
41+
if (owner && ownerStack !== '') {
42+
info += '\n' + ownerStack;
43+
}
44+
} else {
45+
break;
46+
}
47+
}
48+
return info;
49+
} catch (x) {
50+
return '\nError generating stack: ' + x.message + '\n' + x.stack;
51+
}
52+
}

0 commit comments

Comments
 (0)