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.
Sebastian Markbåge committed
Nov 10, 2020 at 19:35 UTC
16e6dadba67a49b4a12271a6c80fab48a4e1a2d4
2 files changed
+71
-2
packages/react-client/src/__tests__/ReactFlight-test.js
+64
@@ -16,6 +16,7 @@ let ReactNoop;
16
let ReactNoopFlightServer;
17
let ReactNoopFlightClient;
18
let ErrorBoundary;
19
+let NoErrorExpected;
20
21
describe('ReactFlight', () => {
22
beforeEach(() => {
@@ -47,6 +48,26 @@ describe('ReactFlight', () => {
48
return this.props.children;
49
}
50
};
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
+ };
71
});
72
73
function moduleReference(value) {
@@ -164,6 +185,49 @@ describe('ReactFlight', () => {
185
});
186
});
187
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
+
231
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
232
expect(() => {
233
const transport = ReactNoopFlightServer.render(
packages/react-server/src/ReactFlightServer.js
+7
-2
@@ -409,8 +409,13 @@ export function resolveModelToJSON(
409
x.then(ping, ping);
410
return serializeByRefID(newSegment.id);
411
} 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);
419
}
420
}
421
}