Skip to content

Commit b798223

Browse files
authored
Override .bind on Server References on the Client (#27282)
That way when you bind arguments to a Server Reference, it's still a server reference and works with progressive enhancement. This already works on the Server (RSC) layer.
1 parent ab31a9e commit b798223

File tree

3 files changed

+115
-18
lines changed

3 files changed

+115
-18
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,9 @@ import {
3333
readPartialStringChunk,
3434
readFinalStringChunk,
3535
createStringDecoder,
36-
usedWithSSR,
3736
} from './ReactFlightClientConfig';
3837

39-
import {
40-
encodeFormAction,
41-
knownServerReferences,
42-
} from './ReactFlightReplyClient';
38+
import {registerServerReference} from './ReactFlightReplyClient';
4339

4440
import {
4541
REACT_LAZY_TYPE,
@@ -545,12 +541,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
545541
return callServer(metaData.id, bound.concat(args));
546542
});
547543
};
548-
// Expose encoder for use by SSR.
549-
if (usedWithSSR) {
550-
// Only expose this in builds that would actually use it. Not needed on the client.
551-
(proxy: any).$$FORM_ACTION = encodeFormAction;
552-
}
553-
knownServerReferences.set(proxy, metaData);
544+
registerServerReference(proxy, metaData);
554545
return proxy;
555546
}
556547

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
4444

4545
export type ServerReferenceId = any;
4646

47-
export const knownServerReferences: WeakMap<
47+
const knownServerReferences: WeakMap<
4848
Function,
4949
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
5050
> = new WeakMap();
@@ -488,6 +488,45 @@ export function encodeFormAction(
488488
};
489489
}
490490

491+
export function registerServerReference(
492+
proxy: any,
493+
reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
494+
) {
495+
// Expose encoder for use by SSR, as well as a special bind that can be used to
496+
// keep server capabilities.
497+
if (usedWithSSR) {
498+
// Only expose this in builds that would actually use it. Not needed on the client.
499+
Object.defineProperties((proxy: any), {
500+
$$FORM_ACTION: {value: encodeFormAction},
501+
bind: {value: bind},
502+
});
503+
}
504+
knownServerReferences.set(proxy, reference);
505+
}
506+
507+
// $FlowFixMe[method-unbinding]
508+
const FunctionBind = Function.prototype.bind;
509+
// $FlowFixMe[method-unbinding]
510+
const ArraySlice = Array.prototype.slice;
511+
function bind(this: Function) {
512+
// $FlowFixMe[unsupported-syntax]
513+
const newFn = FunctionBind.apply(this, arguments);
514+
const reference = knownServerReferences.get(this);
515+
if (reference) {
516+
const args = ArraySlice.call(arguments, 1);
517+
let boundPromise = null;
518+
if (reference.bound !== null) {
519+
boundPromise = Promise.resolve((reference.bound: any)).then(boundArgs =>
520+
boundArgs.concat(args),
521+
);
522+
} else {
523+
boundPromise = Promise.resolve(args);
524+
}
525+
registerServerReference(newFn, {id: reference.id, bound: boundPromise});
526+
}
527+
return newFn;
528+
}
529+
491530
export function createServerReference<A: Iterable<any>, T>(
492531
id: ServerReferenceId,
493532
callServer: CallServerCallback,
@@ -497,11 +536,6 @@ export function createServerReference<A: Iterable<any>, T>(
497536
const args = Array.prototype.slice.call(arguments);
498537
return callServer(id, args);
499538
};
500-
// Expose encoder for use by SSR.
501-
if (usedWithSSR) {
502-
// Only expose this in builds that would actually use it. Not needed on the client.
503-
(proxy: any).$$FORM_ACTION = encodeFormAction;
504-
}
505-
knownServerReferences.set(proxy, {id: id, bound: null});
539+
registerServerReference(proxy, {id, bound: null});
506540
return proxy;
507541
}

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ global.TextDecoder = require('util').TextDecoder;
2222
global.setTimeout = cb => cb();
2323

2424
let container;
25+
let clientExports;
2526
let serverExports;
27+
let webpackMap;
2628
let webpackServerMap;
2729
let React;
2830
let ReactDOMServer;
@@ -37,7 +39,9 @@ describe('ReactFlightDOMForm', () => {
3739
require('react-server-dom-webpack/server.edge'),
3840
);
3941
const WebpackMock = require('./utils/WebpackMock');
42+
clientExports = WebpackMock.clientExports;
4043
serverExports = WebpackMock.serverExports;
44+
webpackMap = WebpackMock.webpackMap;
4145
webpackServerMap = WebpackMock.webpackServerMap;
4246
React = require('react');
4347
ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
@@ -236,4 +240,72 @@ describe('ReactFlightDOMForm', () => {
236240
expect(result).toBe('helloc');
237241
expect(foo).toBe('barc');
238242
});
243+
244+
// @gate enableFormActions
245+
it('can bind an imported server action on the client without hydrating it', async () => {
246+
let foo = null;
247+
248+
const ServerModule = serverExports(function action(bound, formData) {
249+
foo = formData.get('foo') + bound.complex;
250+
return 'hello';
251+
});
252+
const serverAction = ReactServerDOMClient.createServerReference(
253+
ServerModule.$$id,
254+
);
255+
function Client() {
256+
return (
257+
<form action={serverAction.bind(null, {complex: 'object'})}>
258+
<input type="text" name="foo" defaultValue="bar" />
259+
</form>
260+
);
261+
}
262+
263+
const ssrStream = await ReactDOMServer.renderToReadableStream(<Client />);
264+
await readIntoContainer(ssrStream);
265+
266+
const form = container.firstChild;
267+
268+
expect(foo).toBe(null);
269+
270+
const result = await submit(form);
271+
272+
expect(result).toBe('hello');
273+
expect(foo).toBe('barobject');
274+
});
275+
276+
// @gate enableFormActions
277+
it('can bind a server action on the client without hydrating it', async () => {
278+
let foo = null;
279+
280+
const serverAction = serverExports(function action(bound, formData) {
281+
foo = formData.get('foo') + bound.complex;
282+
return 'hello';
283+
});
284+
285+
function Client({action}) {
286+
return (
287+
<form action={action.bind(null, {complex: 'object'})}>
288+
<input type="text" name="foo" defaultValue="bar" />
289+
</form>
290+
);
291+
}
292+
const ClientRef = await clientExports(Client);
293+
294+
const rscStream = ReactServerDOMServer.renderToReadableStream(
295+
<ClientRef action={serverAction} />,
296+
webpackMap,
297+
);
298+
const response = ReactServerDOMClient.createFromReadableStream(rscStream);
299+
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
300+
await readIntoContainer(ssrStream);
301+
302+
const form = container.firstChild;
303+
304+
expect(foo).toBe(null);
305+
306+
const result = await submit(form);
307+
308+
expect(result).toBe('hello');
309+
expect(foo).toBe('barobject');
310+
});
239311
});

0 commit comments

Comments
 (0)