diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index ec0c54bd3527..99cade0bc179 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -15,6 +15,7 @@ import { REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, REACT_PROVIDER_TYPE, + getIteratorFn, } from 'shared/ReactSymbols'; import { @@ -46,6 +47,7 @@ export type ReactServerValue = | number | symbol | null + | void | Iterable | Array | ReactServerObject @@ -69,6 +71,10 @@ function serializeSymbolReference(name: string): string { return '$S' + name; } +function serializeUndefined(): string { + return '$undefined'; +} + function escapeStringValue(value: string): string { if (value[0] === '$') { // We need to escape $ prefixed strings since we use those to encode @@ -154,6 +160,12 @@ export function processReply( ); return serializePromiseID(promiseId); } + if (!isArray(value)) { + const iteratorFn = getIteratorFn(value); + if (iteratorFn) { + return Array.from((value: any)); + } + } if (__DEV__) { if (value !== null && !isArray(value)) { @@ -208,14 +220,14 @@ export function processReply( 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') { const metaData = knownServerReferences.get(value); if (metaData !== undefined) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js new file mode 100644 index 000000000000..c53f8b6f5fc5 --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// let serverExports; +let webpackServerMap; +let ReactServerDOMServer; +let ReactServerDOMClient; + +describe('ReactFlightDOMReply', () => { + beforeEach(() => { + jest.resetModules(); + const WebpackMock = require('./utils/WebpackMock'); + // serverExports = WebpackMock.serverExports; + webpackServerMap = WebpackMock.webpackServerMap; + ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + }); + + it('can pass undefined as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply(undefined); + const missing = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(missing).toBe(undefined); + + const body2 = await ReactServerDOMClient.encodeReply({ + array: [undefined, null, undefined], + prop: undefined, + }); + const object = await ReactServerDOMServer.decodeReply( + body2, + webpackServerMap, + ); + expect(object.array.length).toBe(3); + expect(object.array[0]).toBe(undefined); + expect(object.array[1]).toBe(null); + expect(object.array[3]).toBe(undefined); + expect(object.prop).toBe(undefined); + // These should really be true but our deserialization doesn't currently deal with it. + expect('3' in object.array).toBe(false); + expect('prop' in object).toBe(false); + }); + + it('can pass an iterable as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply({ + [Symbol.iterator]: function* () { + yield 'A'; + yield 'B'; + yield 'C'; + }, + }); + const iterable = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + const items = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const item of iterable) { + items.push(item); + } + expect(items).toEqual(['A', 'B', 'C']); + }); +}); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index b8e7d1817afe..d9de22ca661c 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -397,6 +397,11 @@ function parseModelString( key, ); } + 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);