Repository: facebook/react
Pull request: [Flight Reply] Add undefined and Iterable Support

These were recently added to ReactClientValue and so needs to be reflected in ReactServerValue too.

Changed files:
filename: packages/react-client/src/ReactFlightReplyClient.js
patch: @@ -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<ReactServerValue>
   | Array<ReactServerValue>
   | 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) {

filename: packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
patch: @@ -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']);
+  });
+});

filename: packages/react-server/src/ReactFlightReplyServer.js
patch: @@ -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);