Skip to content

Commit 1830ad2

Browse files
committed
Add a way to create Server Reference Proxies on the client
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. In the future we could expand this to allow .bind() too.
1 parent da6c23a commit 1830ad2

File tree

4 files changed

+96
-3
lines changed

4 files changed

+96
-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/__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)