Skip to content

Commit

Permalink
Encode throwing server components as lazy throwing references
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 committed Nov 10, 2020
1 parent e855f91 commit 6049164
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 6049164

Please sign in to comment.