Skip to content

Commit be353d2

Browse files
authored
[Flight Reply] Add undefined and Iterable Support (#26365)
These were recently added to ReactClientValue and so needs to be reflected in ReactServerValue too.
1 parent ef8bdbe commit be353d2

File tree

3 files changed

+100
-5
lines changed

3 files changed

+100
-5
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
REACT_ELEMENT_TYPE,
1616
REACT_LAZY_TYPE,
1717
REACT_PROVIDER_TYPE,
18+
getIteratorFn,
1819
} from 'shared/ReactSymbols';
1920

2021
import {
@@ -46,6 +47,7 @@ export type ReactServerValue =
4647
| number
4748
| symbol
4849
| null
50+
| void
4951
| Iterable<ReactServerValue>
5052
| Array<ReactServerValue>
5153
| ReactServerObject
@@ -69,6 +71,10 @@ function serializeSymbolReference(name: string): string {
6971
return '$S' + name;
7072
}
7173

74+
function serializeUndefined(): string {
75+
return '$undefined';
76+
}
77+
7278
function escapeStringValue(value: string): string {
7379
if (value[0] === '$') {
7480
// We need to escape $ prefixed strings since we use those to encode
@@ -154,6 +160,12 @@ export function processReply(
154160
);
155161
return serializePromiseID(promiseId);
156162
}
163+
if (!isArray(value)) {
164+
const iteratorFn = getIteratorFn(value);
165+
if (iteratorFn) {
166+
return Array.from((value: any));
167+
}
168+
}
157169

158170
if (__DEV__) {
159171
if (value !== null && !isArray(value)) {
@@ -208,14 +220,14 @@ export function processReply(
208220
return escapeStringValue(value);
209221
}
210222

211-
if (
212-
typeof value === 'boolean' ||
213-
typeof value === 'number' ||
214-
typeof value === 'undefined'
215-
) {
223+
if (typeof value === 'boolean' || typeof value === 'number') {
216224
return value;
217225
}
218226

227+
if (typeof value === 'undefined') {
228+
return serializeUndefined();
229+
}
230+
219231
if (typeof value === 'function') {
220232
const metaData = knownServerReferences.get(value);
221233
if (metaData !== undefined) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
// Polyfills for test environment
13+
global.ReadableStream =
14+
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15+
global.TextEncoder = require('util').TextEncoder;
16+
global.TextDecoder = require('util').TextDecoder;
17+
18+
// let serverExports;
19+
let webpackServerMap;
20+
let ReactServerDOMServer;
21+
let ReactServerDOMClient;
22+
23+
describe('ReactFlightDOMReply', () => {
24+
beforeEach(() => {
25+
jest.resetModules();
26+
const WebpackMock = require('./utils/WebpackMock');
27+
// serverExports = WebpackMock.serverExports;
28+
webpackServerMap = WebpackMock.webpackServerMap;
29+
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
30+
ReactServerDOMClient = require('react-server-dom-webpack/client');
31+
});
32+
33+
it('can pass undefined as a reply', async () => {
34+
const body = await ReactServerDOMClient.encodeReply(undefined);
35+
const missing = await ReactServerDOMServer.decodeReply(
36+
body,
37+
webpackServerMap,
38+
);
39+
expect(missing).toBe(undefined);
40+
41+
const body2 = await ReactServerDOMClient.encodeReply({
42+
array: [undefined, null, undefined],
43+
prop: undefined,
44+
});
45+
const object = await ReactServerDOMServer.decodeReply(
46+
body2,
47+
webpackServerMap,
48+
);
49+
expect(object.array.length).toBe(3);
50+
expect(object.array[0]).toBe(undefined);
51+
expect(object.array[1]).toBe(null);
52+
expect(object.array[3]).toBe(undefined);
53+
expect(object.prop).toBe(undefined);
54+
// These should really be true but our deserialization doesn't currently deal with it.
55+
expect('3' in object.array).toBe(false);
56+
expect('prop' in object).toBe(false);
57+
});
58+
59+
it('can pass an iterable as a reply', async () => {
60+
const body = await ReactServerDOMClient.encodeReply({
61+
[Symbol.iterator]: function* () {
62+
yield 'A';
63+
yield 'B';
64+
yield 'C';
65+
},
66+
});
67+
const iterable = await ReactServerDOMServer.decodeReply(
68+
body,
69+
webpackServerMap,
70+
);
71+
const items = [];
72+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
73+
for (const item of iterable) {
74+
items.push(item);
75+
}
76+
expect(items).toEqual(['A', 'B', 'C']);
77+
});
78+
});

packages/react-server/src/ReactFlightReplyServer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ function parseModelString(
397397
key,
398398
);
399399
}
400+
case 'u': {
401+
// matches "$undefined"
402+
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
403+
return undefined;
404+
}
400405
default: {
401406
// We assume that anything else is a reference ID.
402407
const id = parseInt(value.substring(1), 16);

0 commit comments

Comments
 (0)