@samitouri / QOS-React-2 / commits / 16e6dadba6

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 }