Skip to content

Commit

Permalink
Add a way to create Server Reference Proxies on the client
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage committed Apr 15, 2023
1 parent da6c23a commit 1cc2994
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 3 deletions.
7 changes: 6 additions & 1 deletion packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

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

import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {
knownServerReferences,
createServerReference,
} from './ReactFlightServerReferenceRegistry';

import {
REACT_ELEMENT_TYPE,
Expand Down Expand Up @@ -312,3 +315,5 @@ export function processReply(
}
}
}

export {createServerReference};
15 changes: 15 additions & 0 deletions packages/react-client/src/ReactFlightServerReferenceRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@

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

export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;

type ServerReferenceId = any;

export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();

export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
knownServerReferences.set(proxy, {id: id, bound: null});
return proxy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <A, T>(string, args: A) => Promise<T>;

Expand Down Expand Up @@ -125,4 +128,10 @@ function encodeReply(
});
}

export {createFromXHR, createFromFetch, createFromReadableStream, encodeReply};
export {
createFromXHR,
createFromFetch,
createFromReadableStream,
encodeReply,
createServerReference,
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ function noServerCall() {
);
}

export function createServerReference<A: Iterable<any>, T>(
id: any,
callServer: any,
): (...A) => Promise<T> {
return noServerCall;
}

export type Options = {
moduleMap?: $NonMaybeType<SSRManifest>,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ function noServerCall() {
);
}

export function createServerReference<A: Iterable<any>, T>(
id: any,
callServer: any,
): (...A) => Promise<T> {
return noServerCall;
}

function createFromNodeStream<T>(
stream: Readable,
moduleMap: $NonMaybeType<SSRManifest>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ClientRef action={ServerModuleA.greet} />,
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(<App />);
});
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;

Expand Down

0 comments on commit 1cc2994

Please sign in to comment.