diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 549055acd69de..0b9d6b9f48011 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -33,13 +33,9 @@ import { readPartialStringChunk, readFinalStringChunk, createStringDecoder, - usedWithSSR, } from './ReactFlightClientConfig'; -import { - encodeFormAction, - knownServerReferences, -} from './ReactFlightReplyClient'; +import {registerServerReference} from './ReactFlightReplyClient'; import { REACT_LAZY_TYPE, @@ -545,12 +541,7 @@ function createServerReferenceProxy, 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; } diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 13820c748291a..bedf5a90496f0 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -44,7 +44,7 @@ export type CallServerCallback = (id: any, args: A) => Promise; export type ServerReferenceId = any; -export const knownServerReferences: WeakMap< +const knownServerReferences: WeakMap< Function, {id: ServerReferenceId, bound: null | Thenable>}, > = new WeakMap(); @@ -488,6 +488,45 @@ export function encodeFormAction( }; } +export function registerServerReference( + proxy: any, + reference: {id: ServerReferenceId, bound: null | Thenable>}, +) { + // 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, T>( id: ServerReferenceId, callServer: CallServerCallback, @@ -497,11 +536,6 @@ export function createServerReference, 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; } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index e863dccee6c7b..e66a368a11c93 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -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; @@ -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'); @@ -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 ( +
+ +
+ ); + } + + const ssrStream = await ReactDOMServer.renderToReadableStream(); + 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 ( +
+ +
+ ); + } + const ClientRef = await clientExports(Client); + + const rscStream = ReactServerDOMServer.renderToReadableStream( + , + 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'); + }); });