@samitouri / QOS-React / commits / e81fcfe3f2

[Flight] Expose registerServerReference from the client builds (#32534)

This is used to register Server References that exist in the current environment but also exists in the server it might call into. Such as a remote server. If the value comes from the remote server in the first place then this is called automatically to ensure that you can pass a reference back to where it came from - even if the `serverModuleMap` option is used. This was already the case when `serverModuleMap` wasn't passed. This is how you can pass server references back to the server. However, when we added `serverModuleMap` that pass was skipped because we were getting real functions instead of proxies. For functions that wasn't yet passed from the remote server to the current server, we can register them eagerly just like we do for `import('/server').registerServerReference()`. You can now also do this with `import('/client').registerServerReference()`. We could make them shared so you only have to do this once but it might not be possible to pass to the remote server and the remote server might not even be the same RSC renderer. Therefore I split them. It's up to the compiler whether it should do that or not. It has to know that any function you might call might be able to receive it. This is currently global to a specific RSC renderer.

Sebastian Markbåge committed Mar 5, 2025 at 22:16 UTC e81fcfe3f201a8f626e892fb52ccbd0edba627cb
16 files changed +141 -38
packages/react-client/src/ReactFlightClient.js
+19 -2
@@ -64,7 +64,10 @@ import {
64 rendererPackageName,
65 } from './ReactFlightClientConfig';
66
67 -import {createBoundServerReference} from './ReactFlightReplyClient';
67 +import {
68 + createBoundServerReference,
69 + registerBoundServerReference,
70 +} from './ReactFlightReplyClient';
71
72 import {readTemporaryReference} from './ReactFlightTemporaryReferences';
73
@@ -1096,7 +1099,14 @@ function loadServerReference<A: Iterable<any>, T>(
1099 let promise: null | Thenable<any> = preloadModule(serverReference);
1100 if (!promise) {
1101 if (!metaData.bound) {
1099 - return (requireModule(serverReference): any);
1102 + const resolvedValue = (requireModule(serverReference): any);
1103 + registerBoundServerReference(
1104 + resolvedValue,
1105 + metaData.id,
1106 + metaData.bound,
1107 + response._encodeFormAction,
1108 + );
1109 + return resolvedValue;
1110 } else {
1111 promise = Promise.resolve(metaData.bound);
1112 }
@@ -1128,6 +1138,13 @@ function loadServerReference<A: Iterable<any>, T>(
1138 resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
1139 }
1140
1141 + registerBoundServerReference(
1142 + resolvedValue,
1143 + metaData.id,
1144 + metaData.bound,
1145 + response._encodeFormAction,
1146 + );
1147 +
1148 parentObject[key] = resolvedValue;
1149
1150 // If this is the root object for a model reference, where `handler.value`
packages/react-client/src/ReactFlightReplyClient.js
+18 -8
@@ -1125,11 +1125,12 @@ function createFakeServerFunction<A: Iterable<any>, T>(
1125 }
1126 }
1127
1128 -function registerServerReference(
1129 - proxy: any,
1130 - reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
1128 +export function registerBoundServerReference<T: Function>(
1129 + reference: T,
1130 + id: ServerReferenceId,
1131 + bound: null | Thenable<Array<any>>,
1132 encodeFormAction: void | EncodeFormActionCallback,
1132 -) {
1133 +): void {
1134 // Expose encoder for use by SSR, as well as a special bind that can be used to
1135 // keep server capabilities.
1136 if (usedWithSSR) {
@@ -1147,13 +1148,22 @@ function registerServerReference(
1148 encodeFormAction,
1149 );
1150 };
1150 - Object.defineProperties((proxy: any), {
1151 + Object.defineProperties((reference: any), {
1152 $$FORM_ACTION: {value: $$FORM_ACTION},
1153 $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1154 bind: {value: bind},
1155 });
1156 }
1156 - knownServerReferences.set(proxy, reference);
1157 + knownServerReferences.set(reference, {id, bound});
1158 +}
1159 +
1160 +export function registerServerReference<T: Function>(
1161 + reference: T,
1162 + id: ServerReferenceId,
1163 + encodeFormAction?: EncodeFormActionCallback,
1164 +): ServerReference<T> {
1165 + registerBoundServerReference(reference, id, null, encodeFormAction);
1166 + return reference;
1167 }
1168
1169 // $FlowFixMe[method-unbinding]
@@ -1258,7 +1268,7 @@ export function createBoundServerReference<A: Iterable<any>, T>(
1268 );
1269 }
1270 }
1261 - registerServerReference(action, {id, bound}, encodeFormAction);
1271 + registerBoundServerReference(action, id, bound, encodeFormAction);
1272 return action;
1273 }
1274
@@ -1358,6 +1368,6 @@ export function createServerReference<A: Iterable<any>, T>(
1368 );
1369 }
1370 }
1361 - registerServerReference(action, {id, bound: null}, encodeFormAction);
1371 + registerBoundServerReference(action, id, null, encodeFormAction);
1372 return action;
1373 }
packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+5 -8
@@ -25,9 +25,11 @@ import {
25 injectIntoDevTools,
26 } from 'react-client/src/ReactFlightClient';
27
28 -import {
29 - processReply,
28 +import {processReply} from 'react-client/src/ReactFlightReplyClient';
29 +
30 +export {
31 createServerReference,
32 + registerServerReference,
33 } from 'react-client/src/ReactFlightReplyClient';
34
35 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
@@ -151,12 +153,7 @@ function encodeReply(
153 });
154 }
155
154 -export {
155 - createFromFetch,
156 - createFromReadableStream,
157 - encodeReply,
158 - createServerReference,
159 -};
156 +export {createFromFetch, createFromReadableStream, encodeReply};
157
158 if (__DEV__) {
159 injectIntoDevTools();
packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js
+2
@@ -26,6 +26,8 @@ import {
26
27 import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
28
29 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
30 +
31 function noServerCall() {
32 throw new Error(
33 'Server Functions cannot be called during initial render. ' +
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
+2
@@ -26,6 +26,8 @@ import {
26 createServerReference as createServerReferenceImpl,
27 } from 'react-client/src/ReactFlightReplyClient';
28
29 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
30 +
31 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
32
33 export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js
+2
@@ -25,6 +25,8 @@ import {
25 createServerReference as createServerReferenceImpl,
26 } from 'react-client/src/ReactFlightReplyClient';
27
28 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
29 +
30 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
31
32 export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js
+2
@@ -21,6 +21,8 @@ import {
21
22 import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
23
24 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
25 +
26 function findSourceMapURL(filename: string, environmentName: string) {
27 const devServer = parcelRequire.meta.devServer;
28 if (devServer != null) {
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+5 -8
@@ -25,9 +25,11 @@ import {
25 injectIntoDevTools,
26 } from 'react-client/src/ReactFlightClient';
27
28 -import {
29 - processReply,
28 +import {processReply} from 'react-client/src/ReactFlightReplyClient';
29 +
30 +export {
31 createServerReference,
32 + registerServerReference,
33 } from 'react-client/src/ReactFlightReplyClient';
34
35 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
@@ -150,12 +152,7 @@ function encodeReply(
152 });
153 }
154
153 -export {
154 - createFromFetch,
155 - createFromReadableStream,
156 - encodeReply,
157 - createServerReference,
158 -};
155 +export {createFromFetch, createFromReadableStream, encodeReply};
156
157 if (__DEV__) {
158 injectIntoDevTools();
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js
+2
@@ -41,6 +41,8 @@ import {
41 createServerReference as createServerReferenceImpl,
42 } from 'react-client/src/ReactFlightReplyClient';
43
44 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
45 +
46 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
47
48 export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js
+2
@@ -38,6 +38,8 @@ import {
38
39 import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
40
41 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
42 +
43 function noServerCall() {
44 throw new Error(
45 'Server Functions cannot be called during initial render. ' +
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+42
@@ -301,6 +301,48 @@ describe('ReactFlightDOMEdge', () => {
301 expect(result.boundMethod()).toBe('hi, there');
302 });
303
304 + it('should load a server reference on a consuming server and pass it back', async () => {
305 + function greet(name) {
306 + return 'hi, ' + name;
307 + }
308 + const ServerModule = serverExports({
309 + greet,
310 + });
311 +
312 + const stream = await serverAct(() =>
313 + ReactServerDOMServer.renderToReadableStream(
314 + {
315 + method: ServerModule.greet,
316 + boundMethod: ServerModule.greet.bind(null, 'there'),
317 + },
318 + webpackMap,
319 + ),
320 + );
321 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
322 + serverConsumerManifest: {
323 + moduleMap: webpackMap,
324 + serverModuleMap: webpackServerMap,
325 + moduleLoading: webpackModuleLoading,
326 + },
327 + });
328 +
329 + const result = await response;
330 +
331 + expect(result.method).toBe(greet);
332 + expect(result.boundMethod()).toBe('hi, there');
333 +
334 + const body = await ReactServerDOMClient.encodeReply({
335 + method: result.method,
336 + boundMethod: result.boundMethod,
337 + });
338 + const replyResult = await ReactServerDOMServer.decodeReply(
339 + body,
340 + webpackServerMap,
341 + );
342 + expect(replyResult.method).toBe(greet);
343 + expect(replyResult.boundMethod()).toBe('hi, there');
344 + });
345 +
346 it('should encode long string in a compact format', async () => {
347 const testString = '"\n\t'.repeat(500) + '🙃';
348 const testString2 = 'hello'.repeat(400);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js
+27 -2
@@ -22,7 +22,7 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
22 global.FormData = require('undici').FormData;
23 }
24
25 -// let serverExports;
25 +let serverExports;
26 let webpackServerMap;
27 let ReactServerDOMServer;
28 let ReactServerDOMClient;
@@ -36,7 +36,7 @@ describe('ReactFlightDOMReplyEdge', () => {
36 require('react-server-dom-webpack/server.edge'),
37 );
38 const WebpackMock = require('./utils/WebpackMock');
39 - // serverExports = WebpackMock.serverExports;
39 + serverExports = WebpackMock.serverExports;
40 webpackServerMap = WebpackMock.webpackServerMap;
41 ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
42 jest.resetModules();
@@ -308,4 +308,29 @@ describe('ReactFlightDOMReplyEdge', () => {
308 expect(await decoded.a).toBe('hello');
309 expect(Array.from(await decoded.b)).toEqual(Array.from(buffer));
310 });
311 +
312 + it('can pass a registered server reference', async () => {
313 + function greet(name) {
314 + return 'hi, ' + name;
315 + }
316 + const ServerModule = serverExports({
317 + greet,
318 + });
319 +
320 + ReactServerDOMClient.registerServerReference(
321 + ServerModule.greet,
322 + ServerModule.greet.$$id,
323 + );
324 +
325 + const body = await ReactServerDOMClient.encodeReply({
326 + method: ServerModule.greet,
327 + boundMethod: ServerModule.greet.bind(null, 'there'),
328 + });
329 + const replyResult = await ReactServerDOMServer.decodeReply(
330 + body,
331 + webpackServerMap,
332 + );
333 + expect(replyResult.method).toBe(greet);
334 + expect(replyResult.boundMethod()).toBe('hi, there');
335 + });
336 });
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+5 -8
@@ -25,9 +25,11 @@ import {
25 injectIntoDevTools,
26 } from 'react-client/src/ReactFlightClient';
27
28 -import {
29 - processReply,
28 +import {processReply} from 'react-client/src/ReactFlightReplyClient';
29 +
30 +export {
31 createServerReference,
32 + registerServerReference,
33 } from 'react-client/src/ReactFlightReplyClient';
34
35 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
@@ -150,12 +152,7 @@ function encodeReply(
152 });
153 }
154
153 -export {
154 - createFromFetch,
155 - createFromReadableStream,
156 - encodeReply,
157 - createServerReference,
158 -};
155 +export {createFromFetch, createFromReadableStream, encodeReply};
156
157 if (__DEV__) {
158 injectIntoDevTools();
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js
+2
@@ -41,6 +41,8 @@ import {
41 createServerReference as createServerReferenceImpl,
42 } from 'react-client/src/ReactFlightReplyClient';
43
44 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
45 +
46 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
47
48 export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js
+2
@@ -39,6 +39,8 @@ import {
39
40 import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
41
42 +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
43 +
44 function noServerCall() {
45 throw new Error(
46 'Server Functions cannot be called during initial render. ' +
packages/react-server/src/ReactFlightReplyServer.js
+4 -2
@@ -936,8 +936,10 @@ function parseModelString(
936 // Server Reference
937 const ref = value.slice(2);
938 // TODO: Just encode this in the reference inline instead of as a model.
939 - const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
940 - getOutlinedModel(response, ref, obj, key, createModel);
939 + const metaData: {
940 + id: ServerReferenceId,
941 + bound: null | Thenable<Array<any>>,
942 + } = getOutlinedModel(response, ref, obj, key, createModel);
943 return loadServerReference(
944 response,
945 metaData.id,