[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`
Sebastian Silbermann committed
Mar 9, 2023 at 22:18 UTC
d1ad984db1591b131d16739a24dee4ba44886a09
3 files changed
+47
-5
packages/react-client/src/ReactFlightClient.js
+5
@@ -556,6 +556,11 @@ export function parseModelString(
556
throw chunk.reason;
557
}
558
}
559
+ case 'u': {
560
+ // matches "$undefined"
561
+ // Special encoding for `undefined` which can't be serialized as JSON otherwise.
562
+ return undefined;
563
+ }
564
default: {
565
// We assume that anything else is a reference ID.
566
const id = parseInt(value.substring(1), 16);
packages/react-client/src/__tests__/ReactFlight-test.js
+32
@@ -197,6 +197,38 @@ describe('ReactFlight', () => {
197
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
198
});
199
200
+ it('can render undefined', async () => {
201
+ function Undefined() {
202
+ return undefined;
203
+ }
204
+
205
+ const model = <Undefined />;
206
+
207
+ const transport = ReactNoopFlightServer.render(model);
208
+
209
+ await act(async () => {
210
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
211
+ });
212
+
213
+ expect(ReactNoop).toMatchRenderedOutput(null);
214
+ });
215
+
216
+ it('can render an empty fragment', async () => {
217
+ function Empty() {
218
+ return <React.Fragment />;
219
+ }
220
+
221
+ const model = <Empty />;
222
+
223
+ const transport = ReactNoopFlightServer.render(model);
224
+
225
+ await act(async () => {
226
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
227
+ });
228
+
229
+ expect(ReactNoop).toMatchRenderedOutput(null);
230
+ });
231
+
232
it('can render a lazy component as a shared component on the server', async () => {
233
function SharedComponent({text}) {
234
return (
packages/react-server/src/ReactFlightServer.js
+10
-5
@@ -117,6 +117,7 @@ export type ReactClientValue =
117
| number
118
| symbol
119
| null
120
+ | void
121
| Iterable<ReactClientValue>
122
| Array<ReactClientValue>
123
| ReactClientObject
@@ -546,6 +547,10 @@ function serializeProviderReference(name: string): string {
547
return '$P' + name;
548
}
549
550
+function serializeUndefined(): string {
551
+ return '$undefined';
552
+}
553
+
554
function serializeClientReference(
555
request: Request,
556
parent:
@@ -1134,14 +1139,14 @@ export function resolveModelToJSON(
1139
return escapeStringValue(value);
1140
}
1141
1137
- if (
1138
- typeof value === 'boolean' ||
1139
- typeof value === 'number' ||
1140
- typeof value === 'undefined'
1141
- ) {
1142
+ if (typeof value === 'boolean' || typeof value === 'number') {
1143
return value;
1144
}
1145
1146
+ if (typeof value === 'undefined') {
1147
+ return serializeUndefined();
1148
+ }
1149
+
1150
if (typeof value === 'function') {
1151
if (isClientReference(value)) {
1152
return serializeClientReference(request, parent, key, (value: any));