Skip to content

Commit b600620

Browse files
authored
Add a way to create Server Reference Proxies on the client (#26632)
This lets the client bundle encode Server References without them first being passed from an RSC payload. Like if you just import `"use server"` from the client. A bundler could already emit these proxies to be called on the client but the subtle difference is that those proxies couldn't be passed back into the server by reference. They have to be registered with React. We don't currently implement importing `"use server"` from client components in the reference implementation. It'd need to expand the Webpack plugin with a loader that rewrites files with the `"use server"` in the client bundle. ``` "use server"; export async function action() { ... } ``` -> ``` import {createServerReference} from "react-server-dom-webpack/client"; import {callServer} from "some-router/call-server"; export const action = createServerReference('1234#action', callServer); ``` The technique I use here is that the compiled output has to call `createServerReference(id, callServer)` with the `$$id` and proxy implementation. We then return a proxy function that is registered with a WeakMap to the particular instance of the Flight Client. This might be hard to implement because it requires emitting module imports to a specific stateful runtime module in the compiler. A benefit is that this ensures that this particular reference is locked to a specific client if there are multiple - e.g. talking to different servers. It's fairly arbitrary whether we use a WeakMap technique (like we do on the client) vs an `$$id` (like we do on the server). Not sure what's best overall. The WeakMap is nice because it doesn't leak implementation details that might be abused to consumers. We should probably pick one and unify.
1 parent da6c23a commit b600620

File tree

6 files changed

+110
-3
lines changed

6 files changed

+110
-3
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import type {Thenable} from 'shared/ReactTypes';
1111

12-
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
12+
import {
13+
knownServerReferences,
14+
createServerReference,
15+
} from './ReactFlightServerReferenceRegistry';
1316

1417
import {
1518
REACT_ELEMENT_TYPE,
@@ -312,3 +315,5 @@ export function processReply(
312315
}
313316
}
314317
}
318+
319+
export {createServerReference};

packages/react-client/src/ReactFlightServerReferenceRegistry.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,24 @@
99

1010
import type {Thenable} from 'shared/ReactTypes';
1111

12+
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
13+
1214
type ServerReferenceId = any;
1315

1416
export const knownServerReferences: WeakMap<
1517
Function,
1618
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
1719
> = new WeakMap();
20+
21+
export function createServerReference<A: Iterable<any>, T>(
22+
id: ServerReferenceId,
23+
callServer: CallServerCallback,
24+
): (...A) => Promise<T> {
25+
const proxy = function (): Promise<T> {
26+
// $FlowFixMe[method-unbinding]
27+
const args = Array.prototype.slice.call(arguments);
28+
return callServer(id, args);
29+
};
30+
knownServerReferences.set(proxy, {id: id, bound: null});
31+
return proxy;
32+
}

packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import {
2222
close,
2323
} from 'react-client/src/ReactFlightClientStream';
2424

25-
import {processReply} from 'react-client/src/ReactFlightReplyClient';
25+
import {
26+
processReply,
27+
createServerReference,
28+
} from 'react-client/src/ReactFlightReplyClient';
2629

2730
type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
2831

@@ -125,4 +128,10 @@ function encodeReply(
125128
});
126129
}
127130

128-
export {createFromXHR, createFromFetch, createFromReadableStream, encodeReply};
131+
export {
132+
createFromXHR,
133+
createFromFetch,
134+
createFromReadableStream,
135+
encodeReply,
136+
createServerReference,
137+
};

packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ function noServerCall() {
2929
);
3030
}
3131

32+
export function createServerReference<A: Iterable<any>, T>(
33+
id: any,
34+
callServer: any,
35+
): (...A) => Promise<T> {
36+
return noServerCall;
37+
}
38+
3239
export type Options = {
3340
moduleMap?: $NonMaybeType<SSRManifest>,
3441
};

packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ function noServerCall() {
3232
);
3333
}
3434

35+
export function createServerReference<A: Iterable<any>, T>(
36+
id: any,
37+
callServer: any,
38+
): (...A) => Promise<T> {
39+
return noServerCall;
40+
}
41+
3542
function createFromNodeStream<T>(
3643
stream: Readable,
3744
moduleMap: $NonMaybeType<SSRManifest>,

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,70 @@ describe('ReactFlightDOMBrowser', () => {
893893
expect(result).toBe('Hello Split');
894894
});
895895

896+
it('can pass a server function by importing from client back to server', async () => {
897+
function greet(transform, text) {
898+
return 'Hello ' + transform(text);
899+
}
900+
901+
function upper(text) {
902+
return text.toUpperCase();
903+
}
904+
905+
const ServerModuleA = serverExports({
906+
greet,
907+
});
908+
const ServerModuleB = serverExports({
909+
upper,
910+
});
911+
912+
let actionProxy;
913+
914+
// This is a Proxy representing ServerModuleB in the Client bundle.
915+
const ServerModuleBImportedOnClient = {
916+
upper: ReactServerDOMClient.createServerReference(
917+
ServerModuleB.upper.$$id,
918+
async function (ref, args) {
919+
const body = await ReactServerDOMClient.encodeReply(args);
920+
return callServer(ref, body);
921+
},
922+
),
923+
};
924+
925+
function Client({action}) {
926+
// Client side pass a Server Reference into an action.
927+
actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
928+
return 'Click Me';
929+
}
930+
931+
const ClientRef = clientExports(Client);
932+
933+
const stream = ReactServerDOMServer.renderToReadableStream(
934+
<ClientRef action={ServerModuleA.greet} />,
935+
webpackMap,
936+
);
937+
938+
const response = ReactServerDOMClient.createFromReadableStream(stream, {
939+
async callServer(ref, args) {
940+
const body = await ReactServerDOMClient.encodeReply(args);
941+
return callServer(ref, body);
942+
},
943+
});
944+
945+
function App() {
946+
return use(response);
947+
}
948+
949+
const container = document.createElement('div');
950+
const root = ReactDOMClient.createRoot(container);
951+
await act(() => {
952+
root.render(<App />);
953+
});
954+
expect(container.innerHTML).toBe('Click Me');
955+
956+
const result = await actionProxy('hi');
957+
expect(result).toBe('Hello HI');
958+
});
959+
896960
it('can bind arguments to a server reference', async () => {
897961
let actionProxy;
898962

0 commit comments

Comments
 (0)