[Fizz] Enable owner stacks for SSR (#30152)
Stacked on #30142. This tracks owners and their stacks in DEV in Fizz. We use the ComponentStackNode as the data structure to track this information - effectively like ReactComponentInfo (Server) or Fiber (Client). They're the instance. I then port them same logic from ReactFiberComponentStack, ReactFiberOwnerStack and ReactFiberCallUserSpace to Fizz equivalents. This gets us both owner stacks from `captureOwnerStack()`, as well as appended to console.errors logged by Fizz, while rendering and in onError.
Sebastian Markbåge committed
Jul 1, 2024 at 10:27 UTC
315109b02b0c9460b7466ca88f3f4d6ed1215a21
9 files changed
+727
-111
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+141
-40
@@ -1766,9 +1766,9 @@ describe('ReactDOMFizzServer', () => {
1766
// Intentionally trigger a key warning here.
1767
return (
1768
<div>
1769
- {children.map(t => (
1770
- <span>{t}</span>
1771
- ))}
1769
+ {children.map(function mapper(t) {
1770
+ return <span>{t}</span>;
1771
+ })}
1772
</div>
1773
);
1774
}
@@ -1813,11 +1813,15 @@ describe('ReactDOMFizzServer', () => {
1813
'<%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s',
1814
'inCorrectTag',
1815
'\n' +
1816
- ' in inCorrectTag (at **)\n' +
1817
- ' in C (at **)\n' +
1818
- ' in Suspense (at **)\n' +
1819
- ' in div (at **)\n' +
1820
- ' in A (at **)',
1816
+ (gate(flags => flags.enableOwnerStacks)
1817
+ ? ' in inCorrectTag (at **)\n' +
1818
+ ' in C (at **)\n' +
1819
+ ' in A (at **)'
1820
+ : ' in inCorrectTag (at **)\n' +
1821
+ ' in C (at **)\n' +
1822
+ ' in Suspense (at **)\n' +
1823
+ ' in div (at **)\n' +
1824
+ ' in A (at **)'),
1825
);
1826
mockError.mockClear();
1827
} else {
@@ -1833,22 +1837,19 @@ describe('ReactDOMFizzServer', () => {
1837
expect(mockError).toHaveBeenCalledWith(
1838
'Each child in a list should have a unique "key" prop.%s%s' +
1839
' See https://react.dev/link/warning-keys for more information.%s',
1836
- gate(flags => flags.enableOwnerStacks)
1837
- ? // We currently don't track owners in Fizz which is responsible for this frame.
1838
- ''
1839
- : '\n\nCheck the top-level render call using <div>.',
1840
+ '\n\nCheck the render method of `B`.',
1841
'',
1842
'\n' +
1842
- ' in span (at **)\n' +
1843
- // TODO: Because this validates after the div has been mounted, it is part of
1844
- // the parent stack but since owner stacks will switch to owners this goes away again.
1843
(gate(flags => flags.enableOwnerStacks)
1846
- ? ' in div (at **)\n'
1847
- : '') +
1848
- ' in B (at **)\n' +
1849
- ' in Suspense (at **)\n' +
1850
- ' in div (at **)\n' +
1851
- ' in A (at **)',
1844
+ ? ' in span (at **)\n' +
1845
+ ' in mapper (at **)\n' +
1846
+ ' in B (at **)\n' +
1847
+ ' in A (at **)'
1848
+ : ' in span (at **)\n' +
1849
+ ' in B (at **)\n' +
1850
+ ' in Suspense (at **)\n' +
1851
+ ' in div (at **)\n' +
1852
+ ' in A (at **)'),
1853
);
1854
} else {
1855
expect(mockError).not.toHaveBeenCalled();
@@ -6519,24 +6520,25 @@ describe('ReactDOMFizzServer', () => {
6520
mockError(...args.map(normalizeCodeLocInfo));
6521
};
6522
6523
+ function App() {
6524
+ return (
6525
+ <html>
6526
+ <body>
6527
+ <script>{2}</script>
6528
+ <script>
6529
+ {['try { foo() } catch (e) {} ;', 'try { bar() } catch (e) {} ;']}
6530
+ </script>
6531
+ <script>
6532
+ <MyScript />
6533
+ </script>
6534
+ </body>
6535
+ </html>
6536
+ );
6537
+ }
6538
+
6539
try {
6540
await act(async () => {
6524
- const {pipe} = renderToPipeableStream(
6525
- <html>
6526
- <body>
6527
- <script>{2}</script>
6528
- <script>
6529
- {[
6530
- 'try { foo() } catch (e) {} ;',
6531
- 'try { bar() } catch (e) {} ;',
6532
- ]}
6533
- </script>
6534
- <script>
6535
- <MyScript />
6536
- </script>
6537
- </body>
6538
- </html>,
6539
- );
6541
+ const {pipe} = renderToPipeableStream(<App />);
6542
pipe(writable);
6543
});
6544
@@ -6545,17 +6547,29 @@ describe('ReactDOMFizzServer', () => {
6547
expect(mockError.mock.calls[0]).toEqual([
6548
'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s',
6549
'a number for children',
6548
- componentStack(['script', 'body', 'html']),
6550
+ componentStack(
6551
+ gate(flags => flags.enableOwnerStacks)
6552
+ ? ['script', 'App']
6553
+ : ['script', 'body', 'html', 'App'],
6554
+ ),
6555
]);
6556
expect(mockError.mock.calls[1]).toEqual([
6557
'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s',
6558
'an array for children',
6553
- componentStack(['script', 'body', 'html']),
6559
+ componentStack(
6560
+ gate(flags => flags.enableOwnerStacks)
6561
+ ? ['script', 'App']
6562
+ : ['script', 'body', 'html', 'App'],
6563
+ ),
6564
]);
6565
expect(mockError.mock.calls[2]).toEqual([
6566
'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s',
6567
'something unexpected for children',
6558
- componentStack(['script', 'body', 'html']),
6568
+ componentStack(
6569
+ gate(flags => flags.enableOwnerStacks)
6570
+ ? ['script', 'App']
6571
+ : ['script', 'body', 'html', 'App'],
6572
+ ),
6573
]);
6574
} else {
6575
expect(mockError.mock.calls.length).toBe(0);
@@ -8148,4 +8162,91 @@ describe('ReactDOMFizzServer', () => {
8162
8163
expect(document.body.textContent).toBe('HelloWorld');
8164
});
8165
+
8166
+ // @gate __DEV__ && enableOwnerStacks
8167
+ it('can get the component owner stacks during rendering in dev', async () => {
8168
+ let stack;
8169
+
8170
+ function Foo() {
8171
+ return <Bar />;
8172
+ }
8173
+ function Bar() {
8174
+ return (
8175
+ <div>
8176
+ <Baz />
8177
+ </div>
8178
+ );
8179
+ }
8180
+ function Baz() {
8181
+ stack = React.captureOwnerStack();
8182
+ return <span>hi</span>;
8183
+ }
8184
+
8185
+ await act(() => {
8186
+ const {pipe} = renderToPipeableStream(
8187
+ <div>
8188
+ <Foo />
8189
+ </div>,
8190
+ );
8191
+ pipe(writable);
8192
+ });
8193
+
8194
+ expect(normalizeCodeLocInfo(stack)).toBe(
8195
+ '\n in Bar (at **)' + '\n in Foo (at **)',
8196
+ );
8197
+ });
8198
+
8199
+ // @gate __DEV__ && enableOwnerStacks
8200
+ it('can get the component owner stacks for onError in dev', async () => {
8201
+ const thrownError = new Error('hi');
8202
+ let caughtError;
8203
+ let parentStack;
8204
+ let ownerStack;
8205
+
8206
+ function Foo() {
8207
+ return <Bar />;
8208
+ }
8209
+ function Bar() {
8210
+ return (
8211
+ <div>
8212
+ <Baz />
8213
+ </div>
8214
+ );
8215
+ }
8216
+ function Baz() {
8217
+ throw thrownError;
8218
+ }
8219
+
8220
+ await expect(async () => {
8221
+ await act(() => {
8222
+ const {pipe} = renderToPipeableStream(
8223
+ <div>
8224
+ <Foo />
8225
+ </div>,
8226
+ {
8227
+ onError(error, errorInfo) {
8228
+ caughtError = error;
8229
+ parentStack = errorInfo.componentStack;
8230
+ ownerStack = React.captureOwnerStack
8231
+ ? React.captureOwnerStack()
8232
+ : null;
8233
+ },
8234
+ },
8235
+ );
8236
+ pipe(writable);
8237
+ });
8238
+ }).rejects.toThrow(thrownError);
8239
+
8240
+ expect(caughtError).toBe(thrownError);
8241
+ expect(normalizeCodeLocInfo(parentStack)).toBe(
8242
+ '\n in Baz (at **)' +
8243
+ '\n in div (at **)' +
8244
+ '\n in Bar (at **)' +
8245
+ '\n in Foo (at **)' +
8246
+ '\n in div (at **)',
8247
+ );
8248
+ expect(normalizeCodeLocInfo(ownerStack)).toBe(
8249
+ '\n in Bar (at **)' + '\n in Foo (at **)',
8250
+ );
8251
+ });
8252
});
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+41
-24
@@ -835,21 +835,30 @@ describe('ReactDOMServer', () => {
835
836
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
837
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
838
- ' in span (at **)\n' +
839
- ' in b (at **)\n' +
840
- ' in C (at **)\n' +
841
- ' in font (at **)\n' +
842
- ' in B (at **)\n' +
843
- ' in Child (at **)\n' +
844
- ' in span (at **)\n' +
845
- ' in div (at **)\n' +
846
- ' in App (at **)',
838
+ (gate(flags => flags.enableOwnerStacks)
839
+ ? ' in span (at **)\n' +
840
+ ' in B (at **)\n' +
841
+ ' in Child (at **)\n' +
842
+ ' in App (at **)'
843
+ : ' in span (at **)\n' +
844
+ ' in b (at **)\n' +
845
+ ' in C (at **)\n' +
846
+ ' in font (at **)\n' +
847
+ ' in B (at **)\n' +
848
+ ' in Child (at **)\n' +
849
+ ' in span (at **)\n' +
850
+ ' in div (at **)\n' +
851
+ ' in App (at **)'),
852
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
848
- ' in span (at **)\n' +
849
- ' in Child (at **)\n' +
850
- ' in span (at **)\n' +
851
- ' in div (at **)\n' +
852
- ' in App (at **)',
853
+ (gate(flags => flags.enableOwnerStacks)
854
+ ? ' in span (at **)\n' +
855
+ ' in Child (at **)\n' +
856
+ ' in App (at **)'
857
+ : ' in span (at **)\n' +
858
+ ' in Child (at **)\n' +
859
+ ' in span (at **)\n' +
860
+ ' in div (at **)\n' +
861
+ ' in App (at **)'),
862
]);
863
});
864
@@ -885,9 +894,11 @@ describe('ReactDOMServer', () => {
894
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
895
// ReactDOMServer(App > div > span)
896
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
888
- ' in span (at **)\n' +
889
- ' in div (at **)\n' +
890
- ' in App (at **)',
897
+ (gate(flags => flags.enableOwnerStacks)
898
+ ? ' in span (at **)\n' + ' in App (at **)'
899
+ : ' in span (at **)\n' +
900
+ ' in div (at **)\n' +
901
+ ' in App (at **)'),
902
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
903
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
904
' in blink (at **)',
@@ -898,15 +909,21 @@ describe('ReactDOMServer', () => {
909
' in App2 (at **)',
910
// ReactDOMServer(App > div > Child > span)
911
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
901
- ' in span (at **)\n' +
902
- ' in Child (at **)\n' +
903
- ' in div (at **)\n' +
904
- ' in App (at **)',
912
+ (gate(flags => flags.enableOwnerStacks)
913
+ ? ' in span (at **)\n' +
914
+ ' in Child (at **)\n' +
915
+ ' in App (at **)'
916
+ : ' in span (at **)\n' +
917
+ ' in Child (at **)\n' +
918
+ ' in div (at **)\n' +
919
+ ' in App (at **)'),
920
// ReactDOMServer(App > div > font)
921
'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
907
- ' in font (at **)\n' +
908
- ' in div (at **)\n' +
909
- ' in App (at **)',
922
+ (gate(flags => flags.enableOwnerStacks)
923
+ ? ' in font (at **)\n' + ' in App (at **)'
924
+ : ' in font (at **)\n' +
925
+ ' in div (at **)\n' +
926
+ ' in App (at **)'),
927
]);
928
});
929
packages/react-reconciler/src/ReactInternalTypes.js
+2
-1
@@ -36,6 +36,7 @@ import type {
36
Transition,
37
} from './ReactFiberTracingMarkerComponent';
38
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates';
39
+import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack';
40
41
// Unwind Circular: moved from ReactFiberHooks.old
42
export type HookType =
@@ -439,5 +440,5 @@ export type Dispatcher = {
440
export type AsyncDispatcher = {
441
getCacheForType: <T>(resourceType: () => T) => T,
442
// DEV-only (or !disableStringRefs)
442
- getOwner: () => null | Fiber | ReactComponentInfo,
443
+ getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode,
444
};
packages/react-server/src/ReactFizzAsyncDispatcher.js
+11
-2
@@ -8,9 +8,12 @@
8
*/
9
10
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
11
+import type {ComponentStackNode} from './ReactFizzComponentStack';
12
13
import {disableStringRefs} from 'shared/ReactFeatureFlags';
14
15
+import {currentTaskInDEV} from './ReactFizzCurrentTask';
16
+
17
function getCacheForType<T>(resourceType: () => T): T {
18
throw new Error('Not implemented.');
19
}
@@ -19,8 +22,14 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
22
getCacheForType,
23
}: any);
24
22
-if (__DEV__ || !disableStringRefs) {
23
- // Fizz never tracks owner but the JSX runtime looks for this.
25
+if (__DEV__) {
26
+ DefaultAsyncDispatcher.getOwner = (): ComponentStackNode | null => {
27
+ if (currentTaskInDEV === null) {
28
+ return null;
29
+ }
30
+ return currentTaskInDEV.componentStack;
31
+ };
32
+} else if (!disableStringRefs) {
33
DefaultAsyncDispatcher.getOwner = (): null => {
34
return null;
35
};
packages/react-server/src/ReactFizzCallUserSpace.js
new
+38
@@ -0,0 +1,38 @@
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 {LazyComponent} from 'react/src/ReactLazy';
11
+
12
+// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
13
+// TODO: Consider marking the whole bundle instead of these boundaries.
14
+
15
+/** @noinline */
16
+export function callComponentInDEV<Props, Arg, R>(
17
+ Component: (p: Props, arg: Arg) => R,
18
+ props: Props,
19
+ secondArg: Arg,
20
+): R {
21
+ return Component(props, secondArg);
22
+}
23
+
24
+interface ClassInstance<R> {
25
+ render(): R;
26
+}
27
+
28
+/** @noinline */
29
+export function callRenderInDEV<R>(instance: ClassInstance<R>): R {
30
+ return instance.render();
31
+}
32
+
33
+/** @noinline */
34
+export function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
35
+ const payload = lazy._payload;
36
+ const init = lazy._init;
37
+ return init(payload);
38
+}
packages/react-server/src/ReactFizzComponentStack.js
+91
@@ -7,27 +7,39 @@
7
* @flow
8
*/
9
10
+import type {ReactComponentInfo} from 'shared/ReactTypes';
11
+
12
import {
13
describeBuiltInComponentFrame,
14
describeFunctionComponentFrame,
15
describeClassComponentFrame,
16
} from 'shared/ReactComponentStackFrame';
17
18
+import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
19
+
20
+import {formatOwnerStack} from './ReactFizzOwnerStack';
21
+
22
// DEV-only reverse linked list representing the current component stack
23
type BuiltInComponentStackNode = {
24
tag: 0,
25
parent: null | ComponentStackNode,
26
type: string,
27
+ owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
28
+ stack?: null | string | Error, // DEV only
29
};
30
type FunctionComponentStackNode = {
31
tag: 1,
32
parent: null | ComponentStackNode,
33
type: Function,
34
+ owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
35
+ stack?: null | string | Error, // DEV only
36
};
37
type ClassComponentStackNode = {
38
tag: 2,
39
parent: null | ComponentStackNode,
40
type: Function,
41
+ owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
42
+ stack?: null | string | Error, // DEV only
43
};
44
export type ComponentStackNode =
45
| BuiltInComponentStackNode
@@ -60,3 +72,82 @@ export function getStackByComponentStackNode(
72
return '\nError generating stack: ' + x.message + '\n' + x.stack;
73
}
74
}
75
+
76
+function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string {
77
+ // We use this because we don't actually want to describe the line of the component
78
+ // but just the component name.
79
+ const name = fn ? fn.displayName || fn.name : '';
80
+ return name ? describeBuiltInComponentFrame(name) : '';
81
+}
82
+
83
+export function getOwnerStackByComponentStackNodeInDev(
84
+ componentStack: ComponentStackNode,
85
+): string {
86
+ if (!enableOwnerStacks || !__DEV__) {
87
+ return '';
88
+ }
89
+ try {
90
+ let info = '';
91
+
92
+ // The owner stack of the current component will be where it was created, i.e. inside its owner.
93
+ // There's no actual name of the currently executing component. Instead, that is available
94
+ // on the regular stack that's currently executing. However, for built-ins there is no such
95
+ // named stack frame and it would be ignored as being internal anyway. Therefore we add
96
+ // add one extra frame just to describe the "current" built-in component by name.
97
+ // Similarly, if there is no owner at all, then there's no stack frame so we add the name
98
+ // of the root component to the stack to know which component is currently executing.
99
+ switch (componentStack.tag) {
100
+ case 0:
101
+ info += describeBuiltInComponentFrame(componentStack.type);
102
+ break;
103
+ case 1:
104
+ case 2:
105
+ if (!componentStack.owner) {
106
+ // Only if we have no other data about the callsite do we add
107
+ // the component name as the single stack frame.
108
+ info += describeFunctionComponentFrameWithoutLineNumber(
109
+ componentStack.type,
110
+ );
111
+ }
112
+ break;
113
+ }
114
+
115
+ let owner: void | null | ComponentStackNode | ReactComponentInfo =
116
+ componentStack;
117
+
118
+ while (owner) {
119
+ if (typeof owner.tag === 'number') {
120
+ const node: ComponentStackNode = (owner: any);
121
+ owner = node.owner;
122
+ let debugStack = node.stack;
123
+ // If we don't actually print the stack if there is no owner of this JSX element.
124
+ // In a real app it's typically not useful since the root app is always controlled
125
+ // by the framework. These also tend to have noisy stacks because they're not rooted
126
+ // in a React render but in some imperative bootstrapping code. It could be useful
127
+ // if the element was created in module scope. E.g. hoisted. We could add a a single
128
+ // stack frame for context for example but it doesn't say much if that's a wrapper.
129
+ if (owner && debugStack) {
130
+ if (typeof debugStack !== 'string') {
131
+ // Stash the formatted stack so that we can avoid redoing the filtering.
132
+ node.stack = debugStack = formatOwnerStack(debugStack);
133
+ }
134
+ if (debugStack !== '') {
135
+ info += '\n' + debugStack;
136
+ }
137
+ }
138
+ } else if (typeof owner.stack === 'string') {
139
+ // Server Component
140
+ if (owner.stack !== '') {
141
+ info += '\n' + owner.stack;
142
+ }
143
+ const componentInfo: ReactComponentInfo = (owner: any);
144
+ owner = componentInfo.owner;
145
+ } else {
146
+ break;
147
+ }
148
+ }
149
+ return info;
150
+ } catch (x) {
151
+ return '\nError generating stack: ' + x.message + '\n' + x.stack;
152
+ }
153
+}
packages/react-server/src/ReactFizzCurrentTask.js
new
+19
@@ -0,0 +1,19 @@
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 {Task} from './ReactFizzServer';
11
+
12
+// DEV-only global reference to the currently executing task
13
+export let currentTaskInDEV: null | Task = null;
14
+
15
+export function setCurrentTaskInDEV(task: null | Task): void {
16
+ if (__DEV__) {
17
+ currentTaskInDEV = task;
18
+ }
19
+}
packages/react-server/src/ReactFizzOwnerStack.js
new
+117
@@ -0,0 +1,117 @@
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 {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
11
+
12
+import {
13
+ callLazyInitInDEV,
14
+ callComponentInDEV,
15
+ callRenderInDEV,
16
+} from './ReactFizzCallUserSpace';
17
+
18
+// TODO: Make this configurable on the root.
19
+const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
20
+
21
+let callComponentFrame: null | string = null;
22
+let callIteratorFrame: null | string = null;
23
+let callLazyInitFrame: null | string = null;
24
+
25
+function isNotExternal(stackFrame: string): boolean {
26
+ return !externalRegExp.test(stackFrame);
27
+}
28
+
29
+function initCallComponentFrame(): string {
30
+ // Extract the stack frame of the callComponentInDEV function.
31
+ const error = callComponentInDEV(Error, 'react-stack-top-frame', {});
32
+ const stack = error.stack;
33
+ const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
34
+ const endIdx = stack.indexOf('\n', startIdx);
35
+ if (endIdx === -1) {
36
+ return stack.slice(startIdx);
37
+ }
38
+ return stack.slice(startIdx, endIdx);
39
+}
40
+
41
+function initCallRenderFrame(): string {
42
+ // Extract the stack frame of the callRenderInDEV function.
43
+ try {
44
+ (callRenderInDEV: any)({render: null});
45
+ return '';
46
+ } catch (error) {
47
+ const stack = error.stack;
48
+ const startIdx = stack.startsWith('TypeError: ')
49
+ ? stack.indexOf('\n') + 1
50
+ : 0;
51
+ const endIdx = stack.indexOf('\n', startIdx);
52
+ if (endIdx === -1) {
53
+ return stack.slice(startIdx);
54
+ }
55
+ return stack.slice(startIdx, endIdx);
56
+ }
57
+}
58
+
59
+function initCallLazyInitFrame(): string {
60
+ // Extract the stack frame of the callLazyInitInDEV function.
61
+ const error = callLazyInitInDEV({
62
+ $$typeof: REACT_LAZY_TYPE,
63
+ _init: Error,
64
+ _payload: 'react-stack-top-frame',
65
+ });
66
+ const stack = error.stack;
67
+ const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
68
+ const endIdx = stack.indexOf('\n', startIdx);
69
+ if (endIdx === -1) {
70
+ return stack.slice(startIdx);
71
+ }
72
+ return stack.slice(startIdx, endIdx);
73
+}
74
+
75
+function filterDebugStack(error: Error): string {
76
+ // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
77
+ // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
78
+ // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
79
+ // the DevTools or framework's ignore lists to filter them out.
80
+ let stack = error.stack;
81
+ if (stack.startsWith('Error: react-stack-top-frame\n')) {
82
+ // V8's default formatting prefixes with the error message which we
83
+ // don't want/need.
84
+ stack = stack.slice(29);
85
+ }
86
+ const frames = stack.split('\n').slice(1);
87
+ if (callComponentFrame === null) {
88
+ callComponentFrame = initCallComponentFrame();
89
+ }
90
+ let lastFrameIdx = frames.indexOf(callComponentFrame);
91
+ if (lastFrameIdx === -1) {
92
+ if (callLazyInitFrame === null) {
93
+ callLazyInitFrame = initCallLazyInitFrame();
94
+ }
95
+ lastFrameIdx = frames.indexOf(callLazyInitFrame);
96
+ if (lastFrameIdx === -1) {
97
+ if (callIteratorFrame === null) {
98
+ callIteratorFrame = initCallRenderFrame();
99
+ }
100
+ lastFrameIdx = frames.indexOf(callIteratorFrame);
101
+ }
102
+ }
103
+ if (lastFrameIdx !== -1) {
104
+ // Cut off everything after our "callComponent" slot since it'll be Fiber internals.
105
+ frames.length = lastFrameIdx;
106
+ } else {
107
+ // We didn't find any internal callsite out to user space.
108
+ // This means that this was called outside an owner or the owner is fully internal.
109
+ // To keep things light we exclude the entire trace in this case.
110
+ return '';
111
+ }
112
+ return frames.filter(isNotExternal).join('\n');
113
+}
114
+
115
+export function formatOwnerStack(ownerStackTrace: Error): string {
116
+ return filterDebugStack(ownerStackTrace);
117
+}
packages/react-server/src/ReactFizzServer.js
+267
-44
@@ -20,6 +20,7 @@ import type {
20
Wakeable,
21
Thenable,
22
ReactFormState,
23
+ ReactComponentInfo,
24
} from 'shared/ReactTypes';
25
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
26
import type {
@@ -113,8 +114,17 @@ import {
114
getActionStateMatchingIndex,
115
} from './ReactFizzHooks';
116
import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher';
116
-import {getStackByComponentStackNode} from './ReactFizzComponentStack';
117
+import {
118
+ getStackByComponentStackNode,
119
+ getOwnerStackByComponentStackNodeInDev,
120
+} from './ReactFizzComponentStack';
121
import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
122
+import {currentTaskInDEV, setCurrentTaskInDEV} from './ReactFizzCurrentTask';
123
+import {
124
+ callLazyInitInDEV,
125
+ callComponentInDEV,
126
+ callRenderInDEV,
127
+} from './ReactFizzCallUserSpace';
128
129
import {
130
getIteratorFn,
@@ -790,14 +800,16 @@ function createPendingSegment(
800
};
801
}
802
793
-// DEV-only global reference to the currently executing task
794
-let currentTaskInDEV: null | Task = null;
803
function getCurrentStackInDEV(): string {
804
if (__DEV__) {
805
if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {
806
return '';
807
}
800
- // TODO: Support owner based stacks for logs during SSR.
808
+ if (enableOwnerStacks) {
809
+ return getOwnerStackByComponentStackNodeInDev(
810
+ currentTaskInDEV.componentStack,
811
+ );
812
+ }
813
return getStackByComponentStackNode(currentTaskInDEV.componentStack);
814
}
815
return '';
@@ -810,7 +822,18 @@ function getStackFromNode(stackNode: ComponentStackNode): string {
822
function createBuiltInComponentStack(
823
task: Task,
824
type: string,
825
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
826
+ stack: null | Error, // DEV only
827
): ComponentStackNode {
828
+ if (__DEV__) {
829
+ return {
830
+ tag: 0,
831
+ parent: task.componentStack,
832
+ type,
833
+ owner,
834
+ stack,
835
+ };
836
+ }
837
return {
838
tag: 0,
839
parent: task.componentStack,
@@ -820,7 +843,18 @@ function createBuiltInComponentStack(
843
function createFunctionComponentStack(
844
task: Task,
845
type: Function,
846
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
847
+ stack: null | Error, // DEV only
848
): ComponentStackNode {
849
+ if (__DEV__) {
850
+ return {
851
+ tag: 1,
852
+ parent: task.componentStack,
853
+ type,
854
+ owner,
855
+ stack,
856
+ };
857
+ }
858
return {
859
tag: 1,
860
parent: task.componentStack,
@@ -830,7 +864,18 @@ function createFunctionComponentStack(
864
function createClassComponentStack(
865
task: Task,
866
type: Function,
867
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
868
+ stack: null | Error, // DEV only
869
): ComponentStackNode {
870
+ if (__DEV__) {
871
+ return {
872
+ tag: 2,
873
+ parent: task.componentStack,
874
+ type,
875
+ owner,
876
+ stack,
877
+ };
878
+ }
879
return {
880
tag: 2,
881
parent: task.componentStack,
@@ -841,14 +886,16 @@ function createClassComponentStack(
886
function createComponentStackFromType(
887
task: Task,
888
type: Function | string,
889
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
890
+ stack: null | Error, // DEV only
891
): ComponentStackNode {
892
if (typeof type === 'string') {
846
- return createBuiltInComponentStack(task, type);
893
+ return createBuiltInComponentStack(task, type, owner, stack);
894
}
895
if (shouldConstruct(type)) {
849
- return createClassComponentStack(task, type);
896
+ return createClassComponentStack(task, type, owner, stack);
897
}
851
- return createFunctionComponentStack(task, type);
898
+ return createFunctionComponentStack(task, type, owner, stack);
899
}
900
901
type ThrownInfo = {
@@ -967,6 +1014,8 @@ function renderSuspenseBoundary(
1014
someTask: Task,
1015
keyPath: KeyNode,
1016
props: Object,
1017
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1018
+ stack: null | Error, // DEV only
1019
): void {
1020
if (someTask.replay !== null) {
1021
// If we're replaying through this pass, it means we're replaying through
@@ -989,7 +1038,7 @@ function renderSuspenseBoundary(
1038
// If we end up creating the fallback task we need it to have the correct stack which is
1039
// the stack for the boundary itself. We stash it here so we can use it if needed later
1040
const suspenseComponentStack = (task.componentStack =
992
- createBuiltInComponentStack(task, 'Suspense'));
1041
+ createBuiltInComponentStack(task, 'Suspense', owner, stack));
1042
1043
const prevKeyPath = task.keyPath;
1044
const parentBoundary = task.blockedBoundary;
@@ -1162,12 +1211,14 @@ function replaySuspenseBoundary(
1211
childSlots: ResumeSlots,
1212
fallbackNodes: Array<ReplayNode>,
1213
fallbackSlots: ResumeSlots,
1214
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1215
+ stack: null | Error, // DEV only
1216
): void {
1217
const previousComponentStack = task.componentStack;
1218
// If we end up creating the fallback task we need it to have the correct stack which is
1219
// the stack for the boundary itself. We stash it here so we can use it if needed later
1220
const suspenseComponentStack = (task.componentStack =
1170
- createBuiltInComponentStack(task, 'Suspense'));
1221
+ createBuiltInComponentStack(task, 'Suspense', owner, stack));
1222
1223
const prevKeyPath = task.keyPath;
1224
const previousReplaySet: ReplaySet = task.replay;
@@ -1295,9 +1346,16 @@ function renderBackupSuspenseBoundary(
1346
task: Task,
1347
keyPath: KeyNode,
1348
props: Object,
1349
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1350
+ stack: null | Error, // DEV only
1351
) {
1352
const previousComponentStack = task.componentStack;
1300
- task.componentStack = createBuiltInComponentStack(task, 'Suspense');
1353
+ task.componentStack = createBuiltInComponentStack(
1354
+ task,
1355
+ 'Suspense',
1356
+ owner,
1357
+ stack,
1358
+ );
1359
1360
const content = props.children;
1361
const segment = task.blockedSegment;
@@ -1322,9 +1380,11 @@ function renderHostElement(
1380
keyPath: KeyNode,
1381
type: string,
1382
props: Object,
1383
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1384
+ stack: null | Error, // DEV only
1385
): void {
1386
const previousComponentStack = task.componentStack;
1327
- task.componentStack = createBuiltInComponentStack(task, type);
1387
+ task.componentStack = createBuiltInComponentStack(task, type, owner, stack);
1388
const segment = task.blockedSegment;
1389
if (segment === null) {
1390
// Replay
@@ -1406,7 +1466,12 @@ function renderWithHooks<Props, SecondArg>(
1466
componentIdentity,
1467
prevThenableState,
1468
);
1409
- const result = Component(props, secondArg);
1469
+ let result;
1470
+ if (__DEV__) {
1471
+ result = callComponentInDEV(Component, props, secondArg);
1472
+ } else {
1473
+ result = Component(props, secondArg);
1474
+ }
1475
return finishHooks(Component, props, result, secondArg);
1476
}
1477
@@ -1418,7 +1483,12 @@ function finishClassComponent(
1483
Component: any,
1484
props: any,
1485
): ReactNodeList {
1421
- const nextChildren = instance.render();
1486
+ let nextChildren;
1487
+ if (__DEV__) {
1488
+ nextChildren = callRenderInDEV(instance);
1489
+ } else {
1490
+ nextChildren = instance.render();
1491
+ }
1492
1493
if (__DEV__) {
1494
if (instance.props !== props) {
@@ -1504,10 +1574,17 @@ function renderClassComponent(
1574
keyPath: KeyNode,
1575
Component: any,
1576
props: any,
1577
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1578
+ stack: null | Error, // DEV only
1579
): void {
1580
const resolvedProps = resolveClassComponentProps(Component, props);
1581
const previousComponentStack = task.componentStack;
1510
- task.componentStack = createClassComponentStack(task, Component);
1582
+ task.componentStack = createClassComponentStack(
1583
+ task,
1584
+ Component,
1585
+ owner,
1586
+ stack,
1587
+ );
1588
const maskedContext = !disableLegacyContext
1589
? getMaskedContext(Component, task.legacyContext)
1590
: undefined;
@@ -1542,13 +1619,20 @@ function renderFunctionComponent(
1619
keyPath: KeyNode,
1620
Component: any,
1621
props: any,
1622
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1623
+ stack: null | Error, // DEV only
1624
): void {
1625
let legacyContext;
1626
if (!disableLegacyContext) {
1627
legacyContext = getMaskedContext(Component, task.legacyContext);
1628
}
1629
const previousComponentStack = task.componentStack;
1551
- task.componentStack = createFunctionComponentStack(task, Component);
1630
+ task.componentStack = createFunctionComponentStack(
1631
+ task,
1632
+ Component,
1633
+ owner,
1634
+ stack,
1635
+ );
1636
1637
if (__DEV__) {
1638
if (
@@ -1751,9 +1835,16 @@ function renderForwardRef(
1835
type: any,
1836
props: Object,
1837
ref: any,
1838
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1839
+ stack: null | Error, // DEV only
1840
): void {
1841
const previousComponentStack = task.componentStack;
1756
- task.componentStack = createFunctionComponentStack(task, type.render);
1842
+ task.componentStack = createFunctionComponentStack(
1843
+ task,
1844
+ type.render,
1845
+ owner,
1846
+ stack,
1847
+ );
1848
1849
let propsWithoutRef;
1850
if (enableRefAsProp && 'ref' in props) {
@@ -1803,13 +1894,24 @@ function renderMemo(
1894
type: any,
1895
props: Object,
1896
ref: any,
1897
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1898
+ stack: null | Error, // DEV only
1899
): void {
1900
const innerType = type.type;
1901
const resolvedProps = resolveDefaultPropsOnNonClassComponent(
1902
innerType,
1903
props,
1904
);
1812
- renderElement(request, task, keyPath, innerType, resolvedProps, ref);
1905
+ renderElement(
1906
+ request,
1907
+ task,
1908
+ keyPath,
1909
+ innerType,
1910
+ resolvedProps,
1911
+ ref,
1912
+ owner,
1913
+ stack,
1914
+ );
1915
}
1916
1917
function renderContextConsumer(
@@ -1876,17 +1978,33 @@ function renderLazyComponent(
1978
lazyComponent: LazyComponentType<any, any>,
1979
props: Object,
1980
ref: any,
1981
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
1982
+ stack: null | Error, // DEV only
1983
): void {
1984
const previousComponentStack = task.componentStack;
1881
- task.componentStack = createBuiltInComponentStack(task, 'Lazy');
1882
- const payload = lazyComponent._payload;
1883
- const init = lazyComponent._init;
1884
- const Component = init(payload);
1985
+ task.componentStack = createBuiltInComponentStack(task, 'Lazy', owner, stack);
1986
+ let Component;
1987
+ if (__DEV__) {
1988
+ Component = callLazyInitInDEV(lazyComponent);
1989
+ } else {
1990
+ const payload = lazyComponent._payload;
1991
+ const init = lazyComponent._init;
1992
+ Component = init(payload);
1993
+ }
1994
const resolvedProps = resolveDefaultPropsOnNonClassComponent(
1995
Component,
1996
props,
1997
);
1889
- renderElement(request, task, keyPath, Component, resolvedProps, ref);
1998
+ renderElement(
1999
+ request,
2000
+ task,
2001
+ keyPath,
2002
+ Component,
2003
+ resolvedProps,
2004
+ ref,
2005
+ owner,
2006
+ stack,
2007
+ );
2008
task.componentStack = previousComponentStack;
2009
}
2010
@@ -1917,18 +2035,28 @@ function renderElement(
2035
type: any,
2036
props: Object,
2037
ref: any,
2038
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
2039
+ stack: null | Error, // DEV only
2040
): void {
2041
if (typeof type === 'function') {
2042
if (shouldConstruct(type)) {
1923
- renderClassComponent(request, task, keyPath, type, props);
2043
+ renderClassComponent(request, task, keyPath, type, props, owner, stack);
2044
return;
2045
} else {
1926
- renderFunctionComponent(request, task, keyPath, type, props);
2046
+ renderFunctionComponent(
2047
+ request,
2048
+ task,
2049
+ keyPath,
2050
+ type,
2051
+ props,
2052
+ owner,
2053
+ stack,
2054
+ );
2055
return;
2056
}
2057
}
2058
if (typeof type === 'string') {
1931
- renderHostElement(request, task, keyPath, type, props);
2059
+ renderHostElement(request, task, keyPath, type, props, owner, stack);
2060
return;
2061
}
2062
@@ -1959,7 +2087,12 @@ function renderElement(
2087
}
2088
case REACT_SUSPENSE_LIST_TYPE: {
2089
const preiousComponentStack = task.componentStack;
1962
- task.componentStack = createBuiltInComponentStack(task, 'SuspenseList');
2090
+ task.componentStack = createBuiltInComponentStack(
2091
+ task,
2092
+ 'SuspenseList',
2093
+ owner,
2094
+ stack,
2095
+ );
2096
// TODO: SuspenseList should control the boundaries.
2097
const prevKeyPath = task.keyPath;
2098
task.keyPath = keyPath;
@@ -1983,9 +2116,16 @@ function renderElement(
2116
enableSuspenseAvoidThisFallbackFizz &&
2117
props.unstable_avoidThisFallback === true
2118
) {
1986
- renderBackupSuspenseBoundary(request, task, keyPath, props);
2119
+ renderBackupSuspenseBoundary(
2120
+ request,
2121
+ task,
2122
+ keyPath,
2123
+ props,
2124
+ owner,
2125
+ stack,
2126
+ );
2127
} else {
1988
- renderSuspenseBoundary(request, task, keyPath, props);
2128
+ renderSuspenseBoundary(request, task, keyPath, props, owner, stack);
2129
}
2130
return;
2131
}
@@ -1994,11 +2134,20 @@ function renderElement(
2134
if (typeof type === 'object' && type !== null) {
2135
switch (type.$$typeof) {
2136
case REACT_FORWARD_REF_TYPE: {
1997
- renderForwardRef(request, task, keyPath, type, props, ref);
2137
+ renderForwardRef(
2138
+ request,
2139
+ task,
2140
+ keyPath,
2141
+ type,
2142
+ props,
2143
+ ref,
2144
+ owner,
2145
+ stack,
2146
+ );
2147
return;
2148
}
2149
case REACT_MEMO_TYPE: {
2001
- renderMemo(request, task, keyPath, type, props, ref);
2150
+ renderMemo(request, task, keyPath, type, props, ref, owner, stack);
2151
return;
2152
}
2153
case REACT_PROVIDER_TYPE: {
@@ -2035,7 +2184,16 @@ function renderElement(
2184
// Fall through
2185
}
2186
case REACT_LAZY_TYPE: {
2038
- renderLazyComponent(request, task, keyPath, type, props);
2187
+ renderLazyComponent(
2188
+ request,
2189
+ task,
2190
+ keyPath,
2191
+ type,
2192
+ props,
2193
+ ref,
2194
+ owner,
2195
+ stack,
2196
+ );
2197
return;
2198
}
2199
}
@@ -2115,6 +2273,8 @@ function replayElement(
2273
props: Object,
2274
ref: any,
2275
replay: ReplaySet,
2276
+ owner: null | ReactComponentInfo | ComponentStackNode, // DEV only
2277
+ stack: null | Error, // DEV only
2278
): void {
2279
// We're replaying. Find the path to follow.
2280
const replayNodes = replay.nodes;
@@ -2142,7 +2302,7 @@ function replayElement(
2302
const currentNode = task.node;
2303
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
2304
try {
2145
- renderElement(request, task, keyPath, type, props, ref);
2305
+ renderElement(request, task, keyPath, type, props, ref, owner, stack);
2306
if (
2307
task.replay.pendingTasks === 1 &&
2308
task.replay.nodes.length > 0
@@ -2208,6 +2368,8 @@ function replayElement(
2368
node[3],
2369
node[4] === null ? [] : node[4][2],
2370
node[4] === null ? null : node[4][3],
2371
+ owner,
2372
+ stack,
2373
);
2374
}
2375
// We finished rendering this node, so now we can consume this
@@ -2368,6 +2530,9 @@ function renderNodeDestructive(
2530
ref = element.ref;
2531
}
2532
2533
+ const owner = __DEV__ ? element._owner : null;
2534
+ const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null;
2535
+
2536
const name = getComponentNameFromType(type);
2537
const keyOrIndex =
2538
key == null ? (childIndex === -1 ? 0 : childIndex) : key;
@@ -2389,6 +2554,8 @@ function renderNodeDestructive(
2554
props,
2555
ref,
2556
task.replay,
2557
+ owner,
2558
+ stack,
2559
),
2560
);
2561
return;
@@ -2405,6 +2572,8 @@ function renderNodeDestructive(
2572
props,
2573
ref,
2574
task.replay,
2575
+ owner,
2576
+ stack,
2577
);
2578
// No matches found for this node. We assume it's already emitted in the
2579
// prelude and skip it during the replay.
@@ -2422,12 +2591,14 @@ function renderNodeDestructive(
2591
type,
2592
props,
2593
ref,
2594
+ owner,
2595
+ stack,
2596
),
2597
);
2598
return;
2599
}
2600
}
2430
- renderElement(request, task, keyPath, type, props, ref);
2601
+ renderElement(request, task, keyPath, type, props, ref, owner, stack);
2602
}
2603
return;
2604
}
@@ -2438,11 +2609,21 @@ function renderNodeDestructive(
2609
);
2610
case REACT_LAZY_TYPE: {
2611
const previousComponentStack = task.componentStack;
2441
- task.componentStack = createBuiltInComponentStack(task, 'Lazy');
2612
+ task.componentStack = createBuiltInComponentStack(
2613
+ task,
2614
+ 'Lazy',
2615
+ null,
2616
+ null,
2617
+ );
2618
const lazyNode: LazyComponentType<any, any> = (node: any);
2443
- const payload = lazyNode._payload;
2444
- const init = lazyNode._init;
2445
- const resolvedNode = init(payload);
2619
+ let resolvedNode;
2620
+ if (__DEV__) {
2621
+ resolvedNode = callLazyInitInDEV(lazyNode);
2622
+ } else {
2623
+ const payload = lazyNode._payload;
2624
+ const init = lazyNode._init;
2625
+ resolvedNode = init(payload);
2626
+ }
2627
2628
// We restore the stack before rendering the resolved node because once the Lazy
2629
// has resolved any future errors
@@ -2504,6 +2685,8 @@ function renderNodeDestructive(
2685
task.componentStack = createBuiltInComponentStack(
2686
task,
2687
'AsyncIterable',
2688
+ null,
2689
+ null,
2690
);
2691
2692
// Restore the thenable state before resuming.
@@ -2739,14 +2922,54 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void {
2922
}
2923
didWarnForKey.add(parentStackFrame);
2924
2925
+ const componentName = getComponentNameFromType(child.type);
2926
+ const childOwner = child._owner;
2927
+ const parentOwner = parentStackFrame.owner;
2928
+
2929
+ let currentComponentErrorInfo = '';
2930
+ if (parentOwner && typeof parentOwner.tag === 'number') {
2931
+ const name = getComponentNameFromType((parentOwner: any).type);
2932
+ if (name) {
2933
+ currentComponentErrorInfo =
2934
+ '\n\nCheck the render method of `' + name + '`.';
2935
+ }
2936
+ }
2937
+ if (!currentComponentErrorInfo) {
2938
+ if (componentName) {
2939
+ currentComponentErrorInfo = `\n\nCheck the top-level render call using <${componentName}>.`;
2940
+ }
2941
+ }
2942
+
2943
+ // Usually the current owner is the offender, but if it accepts children as a
2944
+ // property, it may be the creator of the child that's responsible for
2945
+ // assigning it a key.
2946
+ let childOwnerAppendix = '';
2947
+ if (childOwner != null && parentOwner !== childOwner) {
2948
+ let ownerName = null;
2949
+ if (typeof childOwner.tag === 'number') {
2950
+ ownerName = getComponentNameFromType((childOwner: any).type);
2951
+ } else if (typeof childOwner.name === 'string') {
2952
+ ownerName = childOwner.name;
2953
+ }
2954
+ if (ownerName) {
2955
+ // Give the component that originally created this child.
2956
+ childOwnerAppendix = ` It was passed a child from ${ownerName}.`;
2957
+ }
2958
+ }
2959
+
2960
// We create a fake component stack for the child to log the stack trace from.
2743
- const stackFrame = createComponentStackFromType(task, (child: any).type);
2961
+ const stackFrame = createComponentStackFromType(
2962
+ task,
2963
+ (child: any).type,
2964
+ (child: any)._owner,
2965
+ enableOwnerStacks ? (child: any)._debugStack : null,
2966
+ );
2967
task.componentStack = stackFrame;
2968
console.error(
2969
'Each child in a list should have a unique "key" prop.' +
2970
'%s%s See https://react.dev/link/warning-keys for more information.',
2748
- '',
2749
- '',
2971
+ currentComponentErrorInfo,
2972
+ childOwnerAppendix,
2973
);
2974
task.componentStack = stackFrame.parent;
2975
}
@@ -3775,7 +3998,7 @@ function retryRenderTask(
3998
let prevTaskInDEV = null;
3999
if (__DEV__) {
4000
prevTaskInDEV = currentTaskInDEV;
3778
- currentTaskInDEV = task;
4001
+ setCurrentTaskInDEV(task);
4002
}
4003
4004
const childrenLength = segment.children.length;
@@ -3852,7 +4075,7 @@ function retryRenderTask(
4075
return;
4076
} finally {
4077
if (__DEV__) {
3855
- currentTaskInDEV = prevTaskInDEV;
4078
+ setCurrentTaskInDEV(prevTaskInDEV);
4079
}
4080
}
4081
}
@@ -3870,7 +4093,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
4093
let prevTaskInDEV = null;
4094
if (__DEV__) {
4095
prevTaskInDEV = currentTaskInDEV;
3873
- currentTaskInDEV = task;
4096
+ setCurrentTaskInDEV(task);
4097
}
4098
4099
try {
@@ -3939,7 +4162,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
4162
return;
4163
} finally {
4164
if (__DEV__) {
3942
- currentTaskInDEV = prevTaskInDEV;
4165
+ setCurrentTaskInDEV(prevTaskInDEV);
4166
}
4167
}
4168
}