From 47f07158a5b921f6bcd5ed1fad9fd0d7c8a3cb12 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 19 Mar 2024 14:08:20 -0400 Subject: [PATCH 1/2] Add ReplyClientReferences This concept is kind of like a temporary reference but instead of referring to in memory slots it passes client reference metadata back to the server if we got a function from the server in the places as a client reference. This mode only kicks in if temporary references are not available such as in progressive enhancement. However, it allows for passing all values back to the server if they came from there in the first place. --- .../react-client/src/ReactFlightClient.js | 32 ++++++++++--- .../src/ReactFlightReplyClient.js | 48 +++++++++++++++++-- .../forks/ReactFlightClientConfig.custom.js | 2 +- .../forks/ReactFlightClientConfig.dom-bun.js | 2 +- .../ReactFlightClientConfig.dom-legacy.js | 2 +- .../src/ReactFlightClientConfigBundlerESM.js | 2 +- .../src/ReactFlightClientConfigBundlerNode.js | 2 +- ...ReactFlightClientConfigBundlerTurbopack.js | 4 +- .../src/ReactFlightClientConfigBundlerNode.js | 2 +- .../ReactFlightClientConfigBundlerWebpack.js | 2 +- .../src/__tests__/ReactFlightDOMReply-test.js | 31 ++++++++++++ .../src/ReactFlightReplyServer.js | 16 ++++++- .../react-server/src/ReactFlightServer.js | 32 ++++++++++++- .../ReactFlightServerConfigBundlerCustom.js | 2 +- .../ReactFlightServerTemporaryReferences.js | 44 ++++++++++++++++- 15 files changed, 199 insertions(+), 24 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 435ee06aac44b..38d3b0baf69ea 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -55,7 +55,10 @@ import { printToConsole, } from './ReactFlightClientConfig'; -import {registerServerReference} from './ReactFlightReplyClient'; +import { + registerServerReference, + registerClientReference, +} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; @@ -128,7 +131,7 @@ type ResolvedModelChunk = { type ResolvedModuleChunk = { status: 'resolved_module', value: ClientReference, - reason: null, + reason: ClientReferenceMetadata, _response: Response, _debugInfo?: null | ReactDebugInfo, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, @@ -334,9 +337,10 @@ function createResolvedModelChunk( function createResolvedModuleChunk( response: Response, value: ClientReference, + metadata: ClientReferenceMetadata, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODULE, value, null, response); + return new Chunk(RESOLVED_MODULE, value, metadata, response); } function createInitializedTextChunk( @@ -381,6 +385,7 @@ function resolveModelChunk( function resolveModuleChunk( chunk: SomeChunk, value: ClientReference, + metadata: ClientReferenceMetadata, ): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. @@ -391,6 +396,7 @@ function resolveModuleChunk( const resolvedChunk: ResolvedModuleChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODULE; resolvedChunk.value = value; + resolvedChunk.reason = metadata; if (resolveListeners !== null) { initializeModuleChunk(resolvedChunk); wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); @@ -450,9 +456,11 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { function initializeModuleChunk(chunk: ResolvedModuleChunk): void { try { const value: T = requireModule(chunk.value); + registerClientReference(value, chunk.reason); const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; + initializedChunk.reason = null; } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; @@ -949,16 +957,28 @@ function resolveModule( blockedChunk.status = BLOCKED; } promise.then( - () => resolveModuleChunk(blockedChunk, clientReference), + () => + resolveModuleChunk( + blockedChunk, + clientReference, + clientReferenceMetadata, + ), error => triggerErrorOnChunk(blockedChunk, error), ); } else { if (!chunk) { - chunks.set(id, createResolvedModuleChunk(response, clientReference)); + chunks.set( + id, + createResolvedModuleChunk( + response, + clientReference, + clientReferenceMetadata, + ), + ); } else { // This can't actually happen because we don't have any forward // references to modules. - resolveModuleChunk(chunk, clientReference); + resolveModuleChunk(chunk, clientReference, clientReferenceMetadata); } } } diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 5f4bd00deb305..944deb0727546 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -16,6 +16,7 @@ import type { } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; +import type {ClientReferenceMetadata} from './ReactFlightClientConfig'; import {enableRenderableContext} from 'shared/ReactFeatureFlags'; @@ -59,6 +60,9 @@ export type EncodeFormActionCallback = ( args: Promise, ) => ReactCustomFormAction; +const knownClientReferences: WeakMap = + new WeakMap(); + export type ServerReferenceId = any; const knownServerReferences: WeakMap< @@ -97,6 +101,10 @@ function serializePromiseID(id: number): string { return '$@' + id.toString(16); } +function serializeClientReferenceID(id: number): string { + return '$C' + id.toString(16); +} + function serializeServerReferenceID(id: number): string { return '$F' + id.toString(16); } @@ -454,9 +462,12 @@ export function processReply( } if (typeof value === 'function') { - const metaData = knownServerReferences.get(value); - if (metaData !== undefined) { - const metaDataJSON = JSON.stringify(metaData, resolveToJSON); + const serverMetaData = knownServerReferences.get(value); + if (serverMetaData !== undefined) { + const metaDataJSON: string = JSON.stringify( + serverMetaData, + resolveToJSON, + ); if (formData === null) { // Upgrade to use FormData to allow us to stream this value. formData = new FormData(); @@ -468,6 +479,25 @@ export function processReply( return serializeServerReferenceID(refId); } if (temporaryReferences === undefined) { + const clientMetadata = knownClientReferences.get(value); + if (clientMetadata !== undefined) { + // If this function once was loaded by a server response from this same client + // we can pass it back given the same metadata that was sent to us. So that the + // server can return it to us again. + // $FlowFixMe[incompatible-type]: The meta data should be known to never yield undefined. + const metaDataJSON: string = JSON.stringify( + clientMetadata, + resolveToJSON, + ); + if (formData === null) { + // Upgrade to use FormData to allow us to stream this value. + formData = new FormData(); + } + const refId = nextPartId++; + // eslint-disable-next-line react-internal/safe-string-coercion + formData.set(formFieldPrefix + refId, metaDataJSON); + return serializeClientReferenceID(refId); + } throw new Error( 'Client Functions cannot be passed directly to Server Functions. ' + 'Only Functions passed from the Server can be passed back again.', @@ -686,6 +716,18 @@ function isSignatureEqual( } } +export function registerClientReference( + reference: T, + metadata: ClientReferenceMetadata, +): void { + if (typeof reference === 'function') { + // Currently we only track functions, like Components, because objects might be + // serializable and if they are the receiving side might expect to be able to + // use them. Functions on the other hand would error anywayy. + knownClientReferences.set(reference, metadata); + } +} + export function registerServerReference( proxy: any, reference: {id: ServerReferenceId, bound: null | Thenable>}, diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index 45a47a8c1405f..fbe7e0e603c4a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -29,7 +29,7 @@ export opaque type ModuleLoading = mixed; export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = mixed; +export type ClientReferenceMetadata = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export const resolveClientReference = $$$config.resolveClientReference; export const resolveServerReference = $$$config.resolveServerReference; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 50713ae8e8e68..1a9de5405167b 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -16,7 +16,7 @@ export opaque type ModuleLoading = mixed; export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = mixed; +export type ClientReferenceMetadata = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export const resolveClientReference: any = null; export const resolveServerReference: any = null; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 017dc33081d5f..51178f1bc53b2 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -16,7 +16,7 @@ export opaque type ModuleLoading = mixed; export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = mixed; +export type ClientReferenceMetadata = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export const resolveClientReference: any = null; export const resolveServerReference: any = null; diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js index 5db99dc8fff05..1e90b9b759d78 100644 --- a/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js @@ -22,7 +22,7 @@ export type ServerReferenceId = string; import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig'; -export opaque type ClientReferenceMetadata = [ +export type ClientReferenceMetadata = [ string, // module path string, // export name ]; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js index b2bf9b8ae0b53..c11cd049089ac 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js @@ -34,7 +34,7 @@ export type ServerManifest = void; export type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = ImportMetadata; +export type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars export opaque type ClientReference = { diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js index b20ad69a408d6..e29654096fb12 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js @@ -43,10 +43,10 @@ export type ServerManifest = { export type ServerReferenceId = string; export opaque type ClientReferenceManifestEntry = ImportManifestEntry; -export opaque type ClientReferenceMetadata = ImportMetadata; +export type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars -export opaque type ClientReference = ClientReferenceMetadata; +export type ClientReference = ClientReferenceMetadata; // The reason this function needs to defined here in this file instead of just // being exported directly from the TurbopackDestination... file is because the diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js index b2bf9b8ae0b53..c11cd049089ac 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js @@ -34,7 +34,7 @@ export type ServerManifest = void; export type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = ImportMetadata; +export type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars export opaque type ClientReference = { diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js index 754578e8e7b7f..9c5327289b62d 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js @@ -43,7 +43,7 @@ export type ServerManifest = { export type ServerReferenceId = string; export opaque type ClientReferenceManifestEntry = ImportManifestEntry; -export opaque type ClientReferenceMetadata = ImportMetadata; +export type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars export opaque type ClientReference = ClientReferenceMetadata; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 938937dba2afb..54d994444be1b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -15,6 +15,8 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; +let clientExports; +let webpackMap; // let serverExports; let webpackServerMap; let React; @@ -30,6 +32,8 @@ describe('ReactFlightDOMReply', () => { require('react-server-dom-webpack/server.browser'), ); const WebpackMock = require('./utils/WebpackMock'); + clientExports = WebpackMock.clientExports; + webpackMap = WebpackMock.webpackMap; // serverExports = WebpackMock.serverExports; webpackServerMap = WebpackMock.webpackServerMap; React = require('react'); @@ -346,4 +350,31 @@ describe('ReactFlightDOMReply', () => { // This should've been the same reference that we already saw. expect(response.children).toBe(children); }); + + it('can pass a client reference send by the server back again', async () => { + function Component() { + return
; + } + + const ClientComponent = clientExports(Component); + + const stream1 = ReactServerDOMServer.renderToReadableStream( + {component: ClientComponent}, + webpackMap, + ); + const response1 = + await ReactServerDOMClient.createFromReadableStream(stream1); + expect(response1.component).toBe(Component); + + const body = await ReactServerDOMClient.encodeReply({ + replied: response1.component, + }); + const serverPayload = await ReactServerDOMServer.decodeReply(body); + + const stream2 = ReactServerDOMServer.renderToReadableStream(serverPayload); + const response2 = + await ReactServerDOMClient.createFromReadableStream(stream2); + + expect(response2.replied).toBe(Component); + }); }); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index cf308ba5204ea..38aa20a31228d 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -16,6 +16,7 @@ import type { ServerReferenceId, ServerManifest, ClientReference as ServerReference, + ClientReferenceMetadata, } from 'react-client/src/ReactFlightClientConfig'; import { @@ -24,7 +25,10 @@ import { requireModule, } from 'react-client/src/ReactFlightClientConfig'; -import {createTemporaryReference} from './ReactFlightServerTemporaryReferences'; +import { + createTemporaryReference, + createReplyClientReference, +} from './ReactFlightServerTemporaryReferences'; export type JSONValue = | number @@ -419,6 +423,16 @@ function parseModelString( // Temporary Reference return createTemporaryReference(value.slice(2)); } + case 'C': { + // Client Reference + const id = parseInt(value.slice(2), 16); + // TODO: Just encode this in the reference inline instead of as a model. + const metaData: ClientReferenceMetadata = getOutlinedModel( + response, + id, + ); + return createReplyClientReference(metaData); + } case 'Q': { // Map const id = parseInt(value.slice(2), 16); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index d253db44c4a15..a18ea3a331971 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -59,7 +59,10 @@ import type { ReactAsyncInfo, } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; -import type {TemporaryReference} from './ReactFlightServerTemporaryReferences'; +import type { + TemporaryReference, + ReplyClientReference, +} from './ReactFlightServerTemporaryReferences'; import { resolveClientReferenceMetadata, @@ -77,6 +80,8 @@ import { import { isTemporaryReference, resolveTemporaryReferenceID, + isReplyClientReference, + resolveReplyClientReferenceMetadata, } from './ReactFlightServerTemporaryReferences'; import { @@ -794,7 +799,11 @@ function renderElement( } } if (typeof type === 'function') { - if (isClientReference(type) || isTemporaryReference(type)) { + if ( + isClientReference(type) || + isTemporaryReference(type) || + isReplyClientReference(type) + ) { // This is a reference to a Client Component. return renderClientElement(task, type, key, props); } @@ -1057,6 +1066,19 @@ function serializeClientReference( } } +function serializeReplyClientReference( + request: Request, + replyClientReference: ReplyClientReference, +): string { + // TODO: Consider deduping these. + const clientReferenceMetadata: ClientReferenceMetadata = + resolveReplyClientReferenceMetadata(replyClientReference); + request.pendingChunks++; + const importId = request.nextChunkId++; + emitImportChunk(request, importId, clientReferenceMetadata); + return serializeByValueID(importId); +} + function outlineModel(request: Request, value: ReactClientValue): number { const newTask = createTask( request, @@ -1656,6 +1678,9 @@ function renderModelDestructive( if (isTemporaryReference(value)) { return serializeTemporaryReference(request, (value: any)); } + if (isReplyClientReference(value)) { + return serializeReplyClientReference(request, (value: any)); + } if (enableTaint) { const tainted = TaintRegistryObjects.get(value); @@ -2127,6 +2152,9 @@ function renderConsoleValue( if (isTemporaryReference(value)) { return serializeTemporaryReference(request, (value: any)); } + if (isReplyClientReference(value)) { + return serializeReplyClientReference(request, (value: any)); + } // Serialize the body of the function as an eval so it can be printed. // $FlowFixMe[method-unbinding] diff --git a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js index e11c154d05f32..884e95ef6675e 100644 --- a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js +++ b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js @@ -12,7 +12,7 @@ declare var $$$config: any; export opaque type ClientManifest = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export opaque type ServerReference = mixed; // eslint-disable-line no-unused-vars -export opaque type ClientReferenceMetadata: any = mixed; +export type ClientReferenceMetadata = mixed; export opaque type ServerReferenceId: any = mixed; export opaque type ClientReferenceKey: any = mixed; export const isClientReference = $$$config.isClientReference; diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js index c133f9030431d..8cde1962a5256 100644 --- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js +++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js @@ -7,6 +7,26 @@ * @flow */ +import type {ClientReferenceMetadata} from './ReactFlightServerConfig'; + +const REPLY_REFERENCE_TAG = Symbol.for('react.reply.reference'); + +// eslint-disable-next-line no-unused-vars +export opaque type ReplyClientReference = { + $$typeof: symbol, + $$metadata: ClientReferenceMetadata, +}; + +export function isReplyClientReference(reference: Object): boolean { + return reference.$$typeof === REPLY_REFERENCE_TAG; +} + +export function resolveReplyClientReferenceMetadata( + replyClientReference: ReplyClientReference, +): ClientReferenceMetadata { + return replyClientReference.$$metadata; +} + const TEMPORARY_REFERENCE_TAG = Symbol.for('react.temporary.reference'); // eslint-disable-next-line no-unused-vars @@ -39,8 +59,8 @@ const proxyHandlers = { return target.$$typeof; case '$$id': return target.$$id; - case '$$async': - return target.$$async; + case '$$metadata': + return target.$$metadata; case 'name': return undefined; case 'displayName': @@ -97,3 +117,23 @@ export function createTemporaryReference(id: string): TemporaryReference { return new Proxy(reference, proxyHandlers); } + +export function createReplyClientReference( + metadata: ClientReferenceMetadata, +): ReplyClientReference { + const reference: ReplyClientReference = Object.defineProperties( + (function () { + throw new Error( + // eslint-disable-next-line react-internal/safe-string-coercion + `Attempted to call a temporary Client Reference from the server but it is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ); + }: any), + { + $$typeof: {value: REPLY_REFERENCE_TAG}, + $$metadata: {value: metadata}, + }, + ); + return new Proxy(reference, proxyHandlers); +} From 1d26103cd3ba49e74b9780ab049657e6ed88862b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 20 Mar 2024 15:34:57 -0400 Subject: [PATCH 2/2] Reenable JSX elements in Replies --- .../src/ReactFlightReplyClient.js | 17 +++++--- .../src/__tests__/ReactFlightDOMReply-test.js | 42 +++++++++++-------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 944deb0727546..4427e144e1b95 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -227,11 +227,18 @@ export function processReply( switch ((value: any).$$typeof) { case REACT_ELEMENT_TYPE: { if (temporaryReferences === undefined) { - throw new Error( - 'React Element cannot be passed to Server Functions from the Client without a ' + - 'temporary reference set. Pass a TemporaryReferenceSet to the options.' + - (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''), - ); + const element: React$Element = (value: any); + // Serialize as a plain object with a symbol property + // TODO: Consider if we should use a special encoding for this or restore a proper + // element object on the server. E.g. we probably need the _store stuff in case it + // is passed as a child. For now we assume it'll just be passed back to Flight. + return { + $$typeof: REACT_ELEMENT_TYPE, + type: element.type, + key: element.key, + ref: element.ref, + props: element.props, + }; } return serializeTemporaryReferenceID( writeTemporaryReference(temporaryReferences, value), diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 54d994444be1b..fef4f7e740bb8 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -302,24 +302,6 @@ describe('ReactFlightDOMReply', () => { expect(await result2.lazy.value).toBe('Hello'); }); - it('errors when called with JSX by default', async () => { - let error; - try { - await ReactServerDOMClient.encodeReply(
); - } catch (x) { - error = x; - } - expect(error).toEqual( - expect.objectContaining({ - message: __DEV__ - ? expect.stringContaining( - 'React Element cannot be passed to Server Functions from the Client without a temporary reference set.', - ) - : expect.stringContaining(''), - }), - ); - }); - it('can pass JSX through a round trip using temporary references', async () => { function Component() { return
; @@ -377,4 +359,28 @@ describe('ReactFlightDOMReply', () => { expect(response2.replied).toBe(Component); }); + + it('can pass a client JSX sent by the server back again', async () => { + function Component() { + return
; + } + + const ClientComponent = clientExports(Component); + + const stream1 = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + const response1 = + await ReactServerDOMClient.createFromReadableStream(stream1); + + const body = await ReactServerDOMClient.encodeReply(response1); + const serverPayload = await ReactServerDOMServer.decodeReply(body); + + const stream2 = ReactServerDOMServer.renderToReadableStream(serverPayload); + const response2 = + await ReactServerDOMClient.createFromReadableStream(stream2); + + expect(response2.type).toBe(Component); + }); });