Skip to content

Commit 315109b

Browse files
authored
[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.
1 parent ad59ddf commit 315109b

9 files changed

+727
-111
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+141-40
Original file line numberDiff line numberDiff line change
@@ -1766,9 +1766,9 @@ describe('ReactDOMFizzServer', () => {
17661766
// Intentionally trigger a key warning here.
17671767
return (
17681768
<div>
1769-
{children.map(t => (
1770-
<span>{t}</span>
1771-
))}
1769+
{children.map(function mapper(t) {
1770+
return <span>{t}</span>;
1771+
})}
17721772
</div>
17731773
);
17741774
}
@@ -1813,11 +1813,15 @@ describe('ReactDOMFizzServer', () => {
18131813
'<%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s',
18141814
'inCorrectTag',
18151815
'\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 **)'),
18211825
);
18221826
mockError.mockClear();
18231827
} else {
@@ -1833,22 +1837,19 @@ describe('ReactDOMFizzServer', () => {
18331837
expect(mockError).toHaveBeenCalledWith(
18341838
'Each child in a list should have a unique "key" prop.%s%s' +
18351839
' 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`.',
18401841
'',
18411842
'\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.
18451843
(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 **)'),
18521853
);
18531854
} else {
18541855
expect(mockError).not.toHaveBeenCalled();
@@ -6519,24 +6520,25 @@ describe('ReactDOMFizzServer', () => {
65196520
mockError(...args.map(normalizeCodeLocInfo));
65206521
};
65216522

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+
65226539
try {
65236540
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 />);
65406542
pipe(writable);
65416543
});
65426544

@@ -6545,17 +6547,29 @@ describe('ReactDOMFizzServer', () => {
65456547
expect(mockError.mock.calls[0]).toEqual([
65466548
'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',
65476549
'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+
),
65496555
]);
65506556
expect(mockError.mock.calls[1]).toEqual([
65516557
'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',
65526558
'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+
),
65546564
]);
65556565
expect(mockError.mock.calls[2]).toEqual([
65566566
'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',
65576567
'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+
),
65596573
]);
65606574
} else {
65616575
expect(mockError.mock.calls.length).toBe(0);
@@ -8148,4 +8162,91 @@ describe('ReactDOMFizzServer', () => {
81488162

81498163
expect(document.body.textContent).toBe('HelloWorld');
81508164
});
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+
});
81518252
});

packages/react-dom/src/__tests__/ReactServerRendering-test.js

+41-24
Original file line numberDiff line numberDiff line change
@@ -835,21 +835,30 @@ describe('ReactDOMServer', () => {
835835

836836
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
837837
'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 **)'),
847852
'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 **)'),
853862
]);
854863
});
855864

@@ -885,9 +894,11 @@ describe('ReactDOMServer', () => {
885894
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
886895
// ReactDOMServer(App > div > span)
887896
'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 **)'),
891902
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
892903
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
893904
' in blink (at **)',
@@ -898,15 +909,21 @@ describe('ReactDOMServer', () => {
898909
' in App2 (at **)',
899910
// ReactDOMServer(App > div > Child > span)
900911
'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 **)'),
905920
// ReactDOMServer(App > div > font)
906921
'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 **)'),
910927
]);
911928
});
912929

packages/react-reconciler/src/ReactInternalTypes.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
Transition,
3737
} from './ReactFiberTracingMarkerComponent';
3838
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates';
39+
import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack';
3940

4041
// Unwind Circular: moved from ReactFiberHooks.old
4142
export type HookType =
@@ -439,5 +440,5 @@ export type Dispatcher = {
439440
export type AsyncDispatcher = {
440441
getCacheForType: <T>(resourceType: () => T) => T,
441442
// DEV-only (or !disableStringRefs)
442-
getOwner: () => null | Fiber | ReactComponentInfo,
443+
getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode,
443444
};

packages/react-server/src/ReactFizzAsyncDispatcher.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
*/
99

1010
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
11+
import type {ComponentStackNode} from './ReactFizzComponentStack';
1112

1213
import {disableStringRefs} from 'shared/ReactFeatureFlags';
1314

15+
import {currentTaskInDEV} from './ReactFizzCurrentTask';
16+
1417
function getCacheForType<T>(resourceType: () => T): T {
1518
throw new Error('Not implemented.');
1619
}
@@ -19,8 +22,14 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
1922
getCacheForType,
2023
}: any);
2124

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) {
2433
DefaultAsyncDispatcher.getOwner = (): null => {
2534
return null;
2635
};

0 commit comments

Comments
 (0)