Skip to content

Commit b73dcdc

Browse files
authored
[Fizz] Refactor Component Stack Nodes (#30298)
Component stacks have a similar problem to the problem with keyPath where we had to move it down and set it late right before recursing. Currently we work around that by popping exactly one off when something suspends. That doesn't work with the new server stacks being added which are more than one. It also meant that we kept having add a single frame that could be popped when there shouldn't need to be one. Unlike keyPath component stacks has this weird property that once something throws we might need the stack that was attempted for errors or the previous stack if we're going to retry and just recreate it. I've tried a few different approaches and I didn't like either but this is the one that seems least problematic. I first split out renderNodeDestructive into a retryNode helper. During retries only retryNode is called. When we first discover a node, we pass through renderNodeDestructive. Instead of add a component stack frame deep inside renderNodeDestructive after we've already refined a node, we now add it before in renderNodeDestructive. That way it's only added once before being attempted. This is similar to how Fiber works where in ChildFiber we match the node once to create the instance and then later do we attempt to actually render it and it's only the second part that's ever retried. This unfortunately means that we now have to refine the node down to element/lazy/thenables twice. To avoid refining the type too I move that to be done lazily.
1 parent 8aafbcf commit b73dcdc

File tree

9 files changed

+346
-461
lines changed

9 files changed

+346
-461
lines changed

packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function describeFiber(
4747
case HostComponent:
4848
return describeBuiltInComponentFrame(workInProgress.type);
4949
case LazyComponent:
50+
// TODO: When we support Thenables as component types we should rename this.
5051
return describeBuiltInComponentFrame('Lazy');
5152
case SuspenseComponent:
5253
return describeBuiltInComponentFrame('Suspense');

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

+38-19
Original file line numberDiff line numberDiff line change
@@ -700,17 +700,39 @@ describe('ReactDOMFizzServer', () => {
700700

701701
it('should client render a boundary if a lazy component rejects', async () => {
702702
let rejectComponent;
703+
const promise = new Promise((resolve, reject) => {
704+
rejectComponent = reject;
705+
});
703706
const LazyComponent = React.lazy(() => {
704-
return new Promise((resolve, reject) => {
705-
rejectComponent = reject;
706-
});
707+
return promise;
708+
});
709+
710+
const LazyLazy = React.lazy(async () => {
711+
return {
712+
default: LazyComponent,
713+
};
714+
});
715+
716+
function Wrapper({children}) {
717+
return children;
718+
}
719+
const LazyWrapper = React.lazy(() => {
720+
return {
721+
then(callback) {
722+
callback({
723+
default: Wrapper,
724+
});
725+
},
726+
};
707727
});
708728

709729
function App({isClient}) {
710730
return (
711731
<div>
712732
<Suspense fallback={<Text text="Loading..." />}>
713-
{isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}
733+
<LazyWrapper>
734+
{isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}
735+
</LazyWrapper>
714736
</Suspense>
715737
</div>
716738
);
@@ -744,6 +766,7 @@ describe('ReactDOMFizzServer', () => {
744766
});
745767
pipe(writable);
746768
});
769+
747770
expect(loggedErrors).toEqual([]);
748771
expect(bootstrapped).toBe(true);
749772

@@ -772,7 +795,7 @@ describe('ReactDOMFizzServer', () => {
772795
'Switched to client rendering because the server rendering errored:\n\n' +
773796
theError.message,
774797
expectedDigest,
775-
componentStack(['Lazy', 'Suspense', 'div', 'App']),
798+
componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),
776799
],
777800
],
778801
[
@@ -852,13 +875,9 @@ describe('ReactDOMFizzServer', () => {
852875
}
853876

854877
await act(() => {
855-
const {pipe} = renderToPipeableStream(
856-
<App isClient={false} />,
857-
858-
{
859-
onError,
860-
},
861-
);
878+
const {pipe} = renderToPipeableStream(<App isClient={false} />, {
879+
onError,
880+
});
862881
pipe(writable);
863882
});
864883
expect(loggedErrors).toEqual([]);
@@ -896,7 +915,7 @@ describe('ReactDOMFizzServer', () => {
896915
'Switched to client rendering because the server rendering errored:\n\n' +
897916
theError.message,
898917
expectedDigest,
899-
componentStack(['Lazy', 'Suspense', 'div', 'App']),
918+
componentStack(['Suspense', 'div', 'App']),
900919
],
901920
],
902921
[
@@ -1395,13 +1414,13 @@ describe('ReactDOMFizzServer', () => {
13951414
'The render was aborted by the server without a reason.',
13961415
expectedDigest,
13971416
// We get the stack of the task when it was aborted which is why we see `h1`
1398-
componentStack(['h1', 'Suspense', 'div', 'App']),
1417+
componentStack(['AsyncText', 'h1', 'Suspense', 'div', 'App']),
13991418
],
14001419
[
14011420
'Switched to client rendering because the server rendering aborted due to:\n\n' +
14021421
'The render was aborted by the server without a reason.',
14031422
expectedDigest,
1404-
componentStack(['Suspense', 'main', 'div', 'App']),
1423+
componentStack(['AsyncText', 'Suspense', 'main', 'div', 'App']),
14051424
],
14061425
],
14071426
[
@@ -3523,13 +3542,13 @@ describe('ReactDOMFizzServer', () => {
35233542
'Switched to client rendering because the server rendering aborted due to:\n\n' +
35243543
'foobar',
35253544
'a digest',
3526-
componentStack(['Suspense', 'p', 'div', 'App']),
3545+
componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']),
35273546
],
35283547
[
35293548
'Switched to client rendering because the server rendering aborted due to:\n\n' +
35303549
'foobar',
35313550
'a digest',
3532-
componentStack(['Suspense', 'span', 'div', 'App']),
3551+
componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']),
35333552
],
35343553
],
35353554
[
@@ -3606,13 +3625,13 @@ describe('ReactDOMFizzServer', () => {
36063625
'Switched to client rendering because the server rendering aborted due to:\n\n' +
36073626
'uh oh',
36083627
'a digest',
3609-
componentStack(['Suspense', 'p', 'div', 'App']),
3628+
componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']),
36103629
],
36113630
[
36123631
'Switched to client rendering because the server rendering aborted due to:\n\n' +
36133632
'uh oh',
36143633
'a digest',
3615-
componentStack(['Suspense', 'span', 'div', 'App']),
3634+
componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']),
36163635
],
36173636
],
36183637
[

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ describe('ReactDOMFizzServerNode', () => {
585585
let isComplete = false;
586586
let rendered = false;
587587
const promise = new Promise(r => (resolve = r));
588-
function Wait() {
588+
function Wait({prop}) {
589589
if (!hasLoaded) {
590590
throw promise;
591591
}

packages/react-html/src/__tests__/ReactHTMLServer-test.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,7 @@ if (!__EXPERIMENTAL__) {
250250
'\n in Bar (at **)' +
251251
'\n in Foo (at **)' +
252252
'\n in div (at **)'
253-
: '\n in Lazy (at **)' +
254-
'\n in div (at **)' +
255-
'\n in div (at **)',
253+
: '\n in div (at **)' + '\n in div (at **)',
256254
);
257255
expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe(
258256
__DEV__ && gate(flags => flags.enableOwnerStacks)

packages/react-reconciler/src/ReactFiberComponentStack.js

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function describeFiber(fiber: Fiber): string {
3939
case HostComponent:
4040
return describeBuiltInComponentFrame(fiber.type);
4141
case LazyComponent:
42+
// TODO: When we support Thenables as component types we should rename this.
4243
return describeBuiltInComponentFrame('Lazy');
4344
case SuspenseComponent:
4445
return describeBuiltInComponentFrame('Suspense');

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

+70
Original file line numberDiff line numberDiff line change
@@ -930,4 +930,74 @@ describe('ReactFlightDOMEdge', () => {
930930
'\n in Bar (at **)' + '\n in Foo (at **)',
931931
);
932932
});
933+
934+
it('supports server components in ssr component stacks', async () => {
935+
let reject;
936+
const promise = new Promise((_, r) => (reject = r));
937+
async function Erroring() {
938+
await promise;
939+
return 'should not render';
940+
}
941+
942+
const model = {
943+
root: ReactServer.createElement(Erroring),
944+
};
945+
946+
const stream = ReactServerDOMServer.renderToReadableStream(
947+
model,
948+
webpackMap,
949+
{
950+
onError() {},
951+
},
952+
);
953+
954+
const rootModel = await ReactServerDOMClient.createFromReadableStream(
955+
stream,
956+
{
957+
ssrManifest: {
958+
moduleMap: null,
959+
moduleLoading: null,
960+
},
961+
},
962+
);
963+
964+
const errors = [];
965+
const result = ReactDOMServer.renderToReadableStream(
966+
<div>{rootModel.root}</div>,
967+
{
968+
onError(error, {componentStack}) {
969+
errors.push({
970+
error,
971+
componentStack: normalizeCodeLocInfo(componentStack),
972+
});
973+
},
974+
},
975+
);
976+
977+
const theError = new Error('my error');
978+
reject(theError);
979+
980+
const expectedMessage = __DEV__
981+
? 'my error'
982+
: 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
983+
984+
try {
985+
await result;
986+
} catch (x) {
987+
expect(x).toEqual(
988+
expect.objectContaining({
989+
message: expectedMessage,
990+
}),
991+
);
992+
}
993+
994+
expect(errors).toEqual([
995+
{
996+
error: expect.objectContaining({
997+
message: expectedMessage,
998+
}),
999+
componentStack: (__DEV__ ? '\n in Erroring' : '') + '\n in div',
1000+
},
1001+
]);
1002+
});
9331003
});

0 commit comments

Comments
 (0)