From 1cc29946c8b67f0c5ceeb3a43ad4a1837361acfb Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 14 Apr 2023 22:52:01 -0400 Subject: [PATCH] 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. --- .../src/ReactFlightReplyClient.js | 7 +- .../src/ReactFlightServerReferenceRegistry.js | 15 +++++ .../src/ReactFlightDOMClientBrowser.js | 13 +++- .../src/ReactFlightDOMClientEdge.js | 7 ++ .../src/ReactFlightDOMClientNode.js | 7 ++ .../__tests__/ReactFlightDOMBrowser-test.js | 64 +++++++++++++++++++ 6 files changed, 110 insertions(+), 3 deletions(-) diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 6f9581c36ed..18fc2834e1d 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -9,7 +9,10 @@ import type {Thenable} from 'shared/ReactTypes'; -import {knownServerReferences} from './ReactFlightServerReferenceRegistry'; +import { + knownServerReferences, + createServerReference, +} from './ReactFlightServerReferenceRegistry'; import { REACT_ELEMENT_TYPE, @@ -312,3 +315,5 @@ export function processReply( } } } + +export {createServerReference}; diff --git a/packages/react-client/src/ReactFlightServerReferenceRegistry.js b/packages/react-client/src/ReactFlightServerReferenceRegistry.js index 7436a19915d..06ad06e9b3e 100644 --- a/packages/react-client/src/ReactFlightServerReferenceRegistry.js +++ b/packages/react-client/src/ReactFlightServerReferenceRegistry.js @@ -9,9 +9,24 @@ import type {Thenable} from 'shared/ReactTypes'; +export type CallServerCallback = (id: any, args: A) => Promise; + type ServerReferenceId = any; export const knownServerReferences: WeakMap< Function, {id: ServerReferenceId, bound: null | Thenable>}, > = new WeakMap(); + +export function createServerReference, T>( + id: ServerReferenceId, + callServer: CallServerCallback, +): (...A) => Promise { + const proxy = function (): Promise { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments); + return callServer(id, args); + }; + knownServerReferences.set(proxy, {id: id, bound: null}); + return proxy; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index 537b96187a0..f847a636e6c 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -22,7 +22,10 @@ import { close, } from 'react-client/src/ReactFlightClientStream'; -import {processReply} from 'react-client/src/ReactFlightReplyClient'; +import { + processReply, + createServerReference, +} from 'react-client/src/ReactFlightReplyClient'; type CallServerCallback = (string, args: A) => Promise; @@ -125,4 +128,10 @@ function encodeReply( }); } -export {createFromXHR, createFromFetch, createFromReadableStream, encodeReply}; +export { + createFromXHR, + createFromFetch, + createFromReadableStream, + encodeReply, + createServerReference, +}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js index 3cab54060c5..28c9dfb3fb8 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -29,6 +29,13 @@ function noServerCall() { ); } +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return noServerCall; +} + export type Options = { moduleMap?: $NonMaybeType, }; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index aff48cf0737..d9db7682c7e 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -32,6 +32,13 @@ function noServerCall() { ); } +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return noServerCall; +} + function createFromNodeStream( stream: Readable, moduleMap: $NonMaybeType, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 0321c5f75d1..55c55e19f87 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -893,6 +893,70 @@ describe('ReactFlightDOMBrowser', () => { expect(result).toBe('Hello Split'); }); + it('can pass a server function by importing from client back to server', async () => { + function greet(transform, text) { + return 'Hello ' + transform(text); + } + + function upper(text) { + return text.toUpperCase(); + } + + const ServerModuleA = serverExports({ + greet, + }); + const ServerModuleB = serverExports({ + upper, + }); + + let actionProxy; + + // This is a Proxy representing ServerModuleB in the Client bundle. + const ServerModuleBImportedOnClient = { + upper: ReactServerDOMClient.createServerReference( + ServerModuleB.upper.$$id, + async function (ref, args) { + const body = await ReactServerDOMClient.encodeReply(args); + return callServer(ref, body); + }, + ), + }; + + function Client({action}) { + // Client side pass a Server Reference into an action. + actionProxy = text => action(ServerModuleBImportedOnClient.upper, text); + return 'Click Me'; + } + + const ClientRef = clientExports(Client); + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + async callServer(ref, args) { + const body = await ReactServerDOMClient.encodeReply(args); + return callServer(ref, body); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('Click Me'); + + const result = await actionProxy('hi'); + expect(result).toBe('Hello HI'); + }); + it('can bind arguments to a server reference', async () => { let actionProxy;