@samitouri / QOS-React-2 / commits / b798223a62

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.

Sebastian Markbåge committed Aug 25, 2023 at 21:39 UTC b798223a620dad77077d27e53cc99cbd54312d99
3 files changed +115 -18
packages/react-client/src/ReactFlightClient.js
+2 -11
@@ -33,13 +33,9 @@ import {
33 readPartialStringChunk,
34 readFinalStringChunk,
35 createStringDecoder,
36 - usedWithSSR,
36 } from './ReactFlightClientConfig';
37
39 -import {
40 - encodeFormAction,
41 - knownServerReferences,
42 -} from './ReactFlightReplyClient';
38 +import {registerServerReference} from './ReactFlightReplyClient';
39
40 import {
41 REACT_LAZY_TYPE,
@@ -545,12 +541,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
541 return callServer(metaData.id, bound.concat(args));
542 });
543 };
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);
545 return proxy;
546 }
547
packages/react-client/src/ReactFlightReplyClient.js
+41 -7
@@ -44,7 +44,7 @@ export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
44
45 export type ServerReferenceId = any;
46
47 -export const knownServerReferences: WeakMap<
47 +const knownServerReferences: WeakMap<
48 Function,
49 {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
50 > = new WeakMap();
@@ -488,6 +488,45 @@ export function encodeFormAction(
488 };
489 }
490
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 +
530 export function createServerReference<A: Iterable<any>, T>(
531 id: ServerReferenceId,
532 callServer: CallServerCallback,
@@ -497,11 +536,6 @@ export function createServerReference<A: Iterable<any>, T>(
536 const args = Array.prototype.slice.call(arguments);
537 return callServer(id, args);
538 };
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});
540 return proxy;
541 }
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+72
@@ -22,7 +22,9 @@ global.TextDecoder = require('util').TextDecoder;
22 global.setTimeout = cb => cb();
23
24 let container;
25 +let clientExports;
26 let serverExports;
27 +let webpackMap;
28 let webpackServerMap;
29 let React;
30 let ReactDOMServer;
@@ -37,7 +39,9 @@ describe('ReactFlightDOMForm', () => {
39 require('react-server-dom-webpack/server.edge'),
40 );
41 const WebpackMock = require('./utils/WebpackMock');
42 + clientExports = WebpackMock.clientExports;
43 serverExports = WebpackMock.serverExports;
44 + webpackMap = WebpackMock.webpackMap;
45 webpackServerMap = WebpackMock.webpackServerMap;
46 React = require('react');
47 ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
@@ -236,4 +240,72 @@ describe('ReactFlightDOMForm', () => {
240 expect(result).toBe('helloc');
241 expect(foo).toBe('barc');
242 });
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 + });
311 });