[Flight Reply] Add undefined and Iterable Support (#26365)
These were recently added to ReactClientValue and so needs to be reflected in ReactServerValue too.
Sebastian Markbåge committed
Mar 10, 2023 at 12:47 UTC
be353d25153e8459013add4b00989784334538cb
3 files changed
+100
-5
packages/react-client/src/ReactFlightReplyClient.js
+17
-5
@@ -15,6 +15,7 @@ import {
15
REACT_ELEMENT_TYPE,
16
REACT_LAZY_TYPE,
17
REACT_PROVIDER_TYPE,
18
+ getIteratorFn,
19
} from 'shared/ReactSymbols';
20
21
import {
@@ -46,6 +47,7 @@ export type ReactServerValue =
47
| number
48
| symbol
49
| null
50
+ | void
51
| Iterable<ReactServerValue>
52
| Array<ReactServerValue>
53
| ReactServerObject
@@ -69,6 +71,10 @@ function serializeSymbolReference(name: string): string {
71
return '$S' + name;
72
}
73
74
+function serializeUndefined(): string {
75
+ return '$undefined';
76
+}
77
+
78
function escapeStringValue(value: string): string {
79
if (value[0] === '$') {
80
// We need to escape $ prefixed strings since we use those to encode
@@ -154,6 +160,12 @@ export function processReply(
160
);
161
return serializePromiseID(promiseId);
162
}
163
+ if (!isArray(value)) {
164
+ const iteratorFn = getIteratorFn(value);
165
+ if (iteratorFn) {
166
+ return Array.from((value: any));
167
+ }
168
+ }
169
170
if (__DEV__) {
171
if (value !== null && !isArray(value)) {
@@ -208,14 +220,14 @@ export function processReply(
220
return escapeStringValue(value);
221
}
222
211
- if (
212
- typeof value === 'boolean' ||
213
- typeof value === 'number' ||
214
- typeof value === 'undefined'
215
- ) {
223
+ if (typeof value === 'boolean' || typeof value === 'number') {
224
return value;
225
}
226
227
+ if (typeof value === 'undefined') {
228
+ return serializeUndefined();
229
+ }
230
+
231
if (typeof value === 'function') {
232
const metaData = knownServerReferences.get(value);
233
if (metaData !== undefined) {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
new
+78
@@ -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
+5
@@ -397,6 +397,11 @@ function parseModelString(
397
key,
398
);
399
}
400
+ case 'u': {
401
+ // matches "$undefined"
402
+ // Special encoding for `undefined` which can't be serialized as JSON otherwise.
403
+ return undefined;
404
+ }
405
default: {
406
// We assume that anything else is a reference ID.
407
const id = parseInt(value.substring(1), 16);