Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override .bind on Server References on the Client #27282

Merged
merged 1 commit into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,9 @@ import {
readPartialStringChunk,
readFinalStringChunk,
createStringDecoder,
usedWithSSR,
} from './ReactFlightClientConfig';

import {
encodeFormAction,
knownServerReferences,
} from './ReactFlightReplyClient';
import {registerServerReference} from './ReactFlightReplyClient';

import {
REACT_LAZY_TYPE,
Expand Down Expand Up @@ -545,12 +541,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
return callServer(metaData.id, bound.concat(args));
});
};
// Expose encoder for use by SSR.
if (usedWithSSR) {
// Only expose this in builds that would actually use it. Not needed on the client.
(proxy: any).$$FORM_ACTION = encodeFormAction;
}
knownServerReferences.set(proxy, metaData);
registerServerReference(proxy, metaData);
return proxy;
}

Expand Down
48 changes: 41 additions & 7 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;

export type ServerReferenceId = any;

export const knownServerReferences: WeakMap<
const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();
Expand Down Expand Up @@ -488,6 +488,45 @@ export function encodeFormAction(
};
}

export function registerServerReference(
proxy: any,
reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
) {
// Expose encoder for use by SSR, as well as a special bind that can be used to
// keep server capabilities.
if (usedWithSSR) {
// Only expose this in builds that would actually use it. Not needed on the client.
Object.defineProperties((proxy: any), {
$$FORM_ACTION: {value: encodeFormAction},
bind: {value: bind},
});
}
knownServerReferences.set(proxy, reference);
}

// $FlowFixMe[method-unbinding]
const FunctionBind = Function.prototype.bind;
// $FlowFixMe[method-unbinding]
const ArraySlice = Array.prototype.slice;
function bind(this: Function) {
// $FlowFixMe[unsupported-syntax]
const newFn = FunctionBind.apply(this, arguments);
const reference = knownServerReferences.get(this);
if (reference) {
const args = ArraySlice.call(arguments, 1);
let boundPromise = null;
if (reference.bound !== null) {
boundPromise = Promise.resolve((reference.bound: any)).then(boundArgs =>
boundArgs.concat(args),
);
} else {
boundPromise = Promise.resolve(args);
}
registerServerReference(newFn, {id: reference.id, bound: boundPromise});
}
return newFn;
}

export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
Expand All @@ -497,11 +536,6 @@ export function createServerReference<A: Iterable<any>, T>(
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
// Expose encoder for use by SSR.
if (usedWithSSR) {
// Only expose this in builds that would actually use it. Not needed on the client.
(proxy: any).$$FORM_ACTION = encodeFormAction;
}
knownServerReferences.set(proxy, {id: id, bound: null});
registerServerReference(proxy, {id, bound: null});
return proxy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ global.TextDecoder = require('util').TextDecoder;
global.setTimeout = cb => cb();

let container;
let clientExports;
let serverExports;
let webpackMap;
let webpackServerMap;
let React;
let ReactDOMServer;
Expand All @@ -37,7 +39,9 @@ describe('ReactFlightDOMForm', () => {
require('react-server-dom-webpack/server.edge'),
);
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
serverExports = WebpackMock.serverExports;
webpackMap = WebpackMock.webpackMap;
webpackServerMap = WebpackMock.webpackServerMap;
React = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
Expand Down Expand Up @@ -236,4 +240,72 @@ describe('ReactFlightDOMForm', () => {
expect(result).toBe('helloc');
expect(foo).toBe('barc');
});

// @gate enableFormActions
it('can bind an imported server action on the client without hydrating it', async () => {
let foo = null;

const ServerModule = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello';
});
const serverAction = ReactServerDOMClient.createServerReference(
ServerModule.$$id,
);
function Client() {
return (
<form action={serverAction.bind(null, {complex: 'object'})}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}

const ssrStream = await ReactDOMServer.renderToReadableStream(<Client />);
await readIntoContainer(ssrStream);

const form = container.firstChild;

expect(foo).toBe(null);

const result = await submit(form);

expect(result).toBe('hello');
expect(foo).toBe('barobject');
});

// @gate enableFormActions
it('can bind a server action on the client without hydrating it', async () => {
let foo = null;

const serverAction = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello';
});

function Client({action}) {
return (
<form action={action.bind(null, {complex: 'object'})}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const ClientRef = await clientExports(Client);

const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream);
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);

const form = container.firstChild;

expect(foo).toBe(null);

const result = await submit(form);

expect(result).toBe('hello');
expect(foo).toBe('barobject');
});
});