Skip to content

Commit 16e6dad

Browse files
authored
Encode throwing server components as lazy throwing references (#20217)
This ensures that if this server component was the child of a client component that has an error boundary, it doesn't trigger the error until this gets rendered so it happens as deep as possible.
1 parent e855f91 commit 16e6dad

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

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

+64
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let ReactNoop;
1616
let ReactNoopFlightServer;
1717
let ReactNoopFlightClient;
1818
let ErrorBoundary;
19+
let NoErrorExpected;
1920

2021
describe('ReactFlight', () => {
2122
beforeEach(() => {
@@ -47,6 +48,26 @@ describe('ReactFlight', () => {
4748
return this.props.children;
4849
}
4950
};
51+
52+
NoErrorExpected = class extends React.Component {
53+
state = {hasError: false, error: null};
54+
static getDerivedStateFromError(error) {
55+
return {
56+
hasError: true,
57+
error,
58+
};
59+
}
60+
componentDidMount() {
61+
expect(this.state.error).toBe(null);
62+
expect(this.state.hasError).toBe(false);
63+
}
64+
render() {
65+
if (this.state.hasError) {
66+
return this.state.error.message;
67+
}
68+
return this.props.children;
69+
}
70+
};
5071
});
5172

5273
function moduleReference(value) {
@@ -164,6 +185,49 @@ describe('ReactFlight', () => {
164185
});
165186
});
166187

188+
it('should trigger the inner most error boundary inside a client component', () => {
189+
function ServerComponent() {
190+
throw new Error('This was thrown in the server component.');
191+
}
192+
193+
function ClientComponent({children}) {
194+
// This should catch the error thrown by the server component, even though it has already happened.
195+
// We currently need to wrap it in a div because as it's set up right now, a lazy reference will
196+
// throw during reconciliation which will trigger the parent of the error boundary.
197+
// This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
198+
// That's a bug.
199+
return (
200+
<ErrorBoundary expectedMessage="This was thrown in the server component.">
201+
<div>{children}</div>
202+
</ErrorBoundary>
203+
);
204+
}
205+
206+
const ClientComponentReference = moduleReference(ClientComponent);
207+
208+
function Server() {
209+
return (
210+
<ClientComponentReference>
211+
<ServerComponent />
212+
</ClientComponentReference>
213+
);
214+
}
215+
216+
const data = ReactNoopFlightServer.render(<Server />);
217+
218+
function Client({transport}) {
219+
return ReactNoopFlightClient.read(transport);
220+
}
221+
222+
act(() => {
223+
ReactNoop.render(
224+
<NoErrorExpected>
225+
<Client transport={data} />
226+
</NoErrorExpected>,
227+
);
228+
});
229+
});
230+
167231
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
168232
expect(() => {
169233
const transport = ReactNoopFlightServer.render(

packages/react-server/src/ReactFlightServer.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,13 @@ export function resolveModelToJSON(
409409
x.then(ping, ping);
410410
return serializeByRefID(newSegment.id);
411411
} else {
412-
// Something errored. Don't bother encoding anything up to here.
413-
throw x;
412+
// Something errored. We'll still send everything we have up until this point.
413+
// We'll replace this element with a lazy reference that throws on the client
414+
// once it gets rendered.
415+
request.pendingChunks++;
416+
const errorId = request.nextChunkId++;
417+
emitErrorChunk(request, errorId, x);
418+
return serializeByRefID(errorId);
414419
}
415420
}
416421
}

0 commit comments

Comments
 (0)