@samitouri / QOS-React-1 / commits / 315109b02b

[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 }