From d1ad984db1591b131d16739a24dee4ba44886a09 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 9 Mar 2023 22:18:52 +0100 Subject: [PATCH] [Flight] Add support for returning `undefined` from render (#26349) ## Summary Adds support for returning `undefined` from Server Components. Also fixes a bug where rendering an empty fragment would throw the same error as returning undefined. ## How did you test this change? - [x] test failed with same error message I got when returning undefined from Server Components in a Next.js app - [x] test passes after adding encoding for `undefined` --- .../react-client/src/ReactFlightClient.js | 5 +++ .../src/__tests__/ReactFlight-test.js | 32 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 15 ++++++--- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index fe5532de5c339..ea6a09619edf1 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -556,6 +556,11 @@ export function parseModelString( throw chunk.reason; } } + case 'u': { + // matches "$undefined" + // Special encoding for `undefined` which can't be serialized as JSON otherwise. + return undefined; + } default: { // We assume that anything else is a reference ID. const id = parseInt(value.substring(1), 16); diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 0e507876d965b..be92bb2d9eb01 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -197,6 +197,38 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(ABC); }); + it('can render undefined', async () => { + function Undefined() { + return undefined; + } + + const model = ; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(null); + }); + + it('can render an empty fragment', async () => { + function Empty() { + return ; + } + + const model = ; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(null); + }); + it('can render a lazy component as a shared component on the server', async () => { function SharedComponent({text}) { return ( diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 2f82e024b67fb..5f7319972d8bf 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -117,6 +117,7 @@ export type ReactClientValue = | number | symbol | null + | void | Iterable | Array | ReactClientObject @@ -546,6 +547,10 @@ function serializeProviderReference(name: string): string { return '$P' + name; } +function serializeUndefined(): string { + return '$undefined'; +} + function serializeClientReference( request: Request, parent: @@ -1134,14 +1139,14 @@ export function resolveModelToJSON( return escapeStringValue(value); } - if ( - typeof value === 'boolean' || - typeof value === 'number' || - typeof value === 'undefined' - ) { + if (typeof value === 'boolean' || typeof value === 'number') { return value; } + if (typeof value === 'undefined') { + return serializeUndefined(); + } + if (typeof value === 'function') { if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any));