Skip to content

Commit

Permalink
Encode throwing server components as lazy throwing references (#20217)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage authored Nov 11, 2020
1 parent e855f91 commit 16e6dad
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 2 deletions.
64 changes: 64 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
let ErrorBoundary;
let NoErrorExpected;

describe('ReactFlight', () => {
beforeEach(() => {
Expand Down Expand Up @@ -47,6 +48,26 @@ describe('ReactFlight', () => {
return this.props.children;
}
};

NoErrorExpected = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
componentDidMount() {
expect(this.state.error).toBe(null);
expect(this.state.hasError).toBe(false);
}
render() {
if (this.state.hasError) {
return this.state.error.message;
}
return this.props.children;
}
};
});

function moduleReference(value) {
Expand Down Expand Up @@ -164,6 +185,49 @@ describe('ReactFlight', () => {
});
});

it('should trigger the inner most error boundary inside a client component', () => {
function ServerComponent() {
throw new Error('This was thrown in the server component.');
}

function ClientComponent({children}) {
// This should catch the error thrown by the server component, even though it has already happened.
// We currently need to wrap it in a div because as it's set up right now, a lazy reference will
// throw during reconciliation which will trigger the parent of the error boundary.
// This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
// That's a bug.
return (
<ErrorBoundary expectedMessage="This was thrown in the server component.">
<div>{children}</div>
</ErrorBoundary>
);
}

const ClientComponentReference = moduleReference(ClientComponent);

function Server() {
return (
<ClientComponentReference>
<ServerComponent />
</ClientComponentReference>
);
}

const data = ReactNoopFlightServer.render(<Server />);

function Client({transport}) {
return ReactNoopFlightClient.read(transport);
}

act(() => {
ReactNoop.render(
<NoErrorExpected>
<Client transport={data} />
</NoErrorExpected>,
);
});
});

it('should warn in DEV if a toJSON instance is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(
Expand Down
9 changes: 7 additions & 2 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,13 @@ export function resolveModelToJSON(
x.then(ping, ping);
return serializeByRefID(newSegment.id);
} else {
// Something errored. Don't bother encoding anything up to here.
throw x;
// Something errored. We'll still send everything we have up until this point.
// We'll replace this element with a lazy reference that throws on the client
// once it gets rendered.
request.pendingChunks++;
const errorId = request.nextChunkId++;
emitErrorChunk(request, errorId, x);
return serializeByRefID(errorId);
}
}
}
Expand Down

0 comments on commit 16e6dad

Please sign in to comment.