@samitouri / QOS-React-1 / commits / 8d48183291

[Flight] Allow custom encoding of the form action (#27563)

There are three parts to an RSC set up: - React - Bundler - Endpoints Most customizability is in the bundler configs. We deal with those as custom builds. To create a full set up, you need to also configure ways to expose end points for example to call a Server Action. That's typically not something the bundler is responsible for even though it's responsible for gathering the end points that needs generation. Exposing which endpoints to generate is a responsibility for the bundler. Typically a meta-framework is responsible for generating the end points. There's two ways to "call" a Server Action. Through JS and through a Form. Through JS we expose the `callServer` callback so that the framework can call the end point. Forms by default POST back to the current page with an action serialized into form data, which we have a decoder helper for. However, this is not something that React is really opinionated about just like we're not opinionated about the protocol used by callServer. This exposes an option to configure the encoding of the form props. `encodeFormAction` is to the SSR is what `callServer` is to the Browser.

Sebastian Markbåge committed Feb 12, 2024 at 20:02 UTC 8d48183291870898ec42ac1f84482d9d26789424
10 files changed +110 -16
packages/react-client/src/ReactFlightClient.js
+9 -3
@@ -28,7 +28,10 @@ import type {
28 HintModel,
29 } from 'react-server/src/ReactFlightServerConfig';
30
31 -import type {CallServerCallback} from './ReactFlightReplyClient';
31 +import type {
32 + CallServerCallback,
33 + EncodeFormActionCallback,
34 +} from './ReactFlightReplyClient';
35
36 import type {Postpone} from 'react/src/ReactPostpone';
37
@@ -53,7 +56,7 @@ import {
56 REACT_POSTPONE_TYPE,
57 } from 'shared/ReactSymbols';
58
56 -export type {CallServerCallback};
59 +export type {CallServerCallback, EncodeFormActionCallback};
60
61 type UninitializedModel = string;
62
@@ -206,6 +209,7 @@ export type Response = {
209 _bundlerConfig: SSRModuleMap,
210 _moduleLoading: ModuleLoading,
211 _callServer: CallServerCallback,
212 + _encodeFormAction: void | EncodeFormActionCallback,
213 _nonce: ?string,
214 _chunks: Map<number, SomeChunk<any>>,
215 _fromJSON: (key: string, value: JSONValue) => any,
@@ -592,7 +596,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
596 },
597 );
598 };
595 - registerServerReference(proxy, metaData);
599 + registerServerReference(proxy, metaData, response._encodeFormAction);
600 return proxy;
601 }
602
@@ -785,6 +789,7 @@ export function createResponse(
789 bundlerConfig: SSRModuleMap,
790 moduleLoading: ModuleLoading,
791 callServer: void | CallServerCallback,
792 + encodeFormAction: void | EncodeFormActionCallback,
793 nonce: void | string,
794 ): Response {
795 const chunks: Map<number, SomeChunk<any>> = new Map();
@@ -792,6 +797,7 @@ export function createResponse(
797 _bundlerConfig: bundlerConfig,
798 _moduleLoading: moduleLoading,
799 _callServer: callServer !== undefined ? callServer : missingCall,
800 + _encodeFormAction: encodeFormAction,
801 _nonce: nonce,
802 _chunks: chunks,
803 _stringDecoder: createStringDecoder(),
packages/react-client/src/ReactFlightReplyClient.js
+54 -5
@@ -47,6 +47,11 @@ export opaque type ServerReference<T> = T;
47
48 export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
49
50 +export type EncodeFormActionCallback = <A>(
51 + id: any,
52 + args: Promise<A>,
53 +) => ReactCustomFormAction;
54 +
55 export type ServerReferenceId = any;
56
57 const knownServerReferences: WeakMap<
@@ -454,7 +459,7 @@ function encodeFormData(reference: any): Thenable<FormData> {
459 return thenable;
460 }
461
457 -export function encodeFormAction(
462 +function defaultEncodeFormAction(
463 this: any => Promise<any>,
464 identifierPrefix: string,
465 ): ReactCustomFormAction {
@@ -503,6 +508,25 @@ export function encodeFormAction(
508 };
509 }
510
511 +function customEncodeFormAction(
512 + proxy: any => Promise<any>,
513 + identifierPrefix: string,
514 + encodeFormAction: EncodeFormActionCallback,
515 +): ReactCustomFormAction {
516 + const reference = knownServerReferences.get(proxy);
517 + if (!reference) {
518 + throw new Error(
519 + 'Tried to encode a Server Action from a different instance than the encoder is from. ' +
520 + 'This is a bug in React.',
521 + );
522 + }
523 + let boundPromise: Promise<Array<any>> = (reference.bound: any);
524 + if (boundPromise === null) {
525 + boundPromise = Promise.resolve([]);
526 + }
527 + return encodeFormAction(reference.id, boundPromise);
528 +}
529 +
530 function isSignatureEqual(
531 this: any => Promise<any>,
532 referenceId: ServerReferenceId,
@@ -569,13 +593,27 @@ function isSignatureEqual(
593 export function registerServerReference(
594 proxy: any,
595 reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
596 + encodeFormAction: void | EncodeFormActionCallback,
597 ) {
598 // Expose encoder for use by SSR, as well as a special bind that can be used to
599 // keep server capabilities.
600 if (usedWithSSR) {
601 // Only expose this in builds that would actually use it. Not needed on the client.
602 + const $$FORM_ACTION =
603 + encodeFormAction === undefined
604 + ? defaultEncodeFormAction
605 + : function (
606 + this: any => Promise<any>,
607 + identifierPrefix: string,
608 + ): ReactCustomFormAction {
609 + return customEncodeFormAction(
610 + this,
611 + identifierPrefix,
612 + encodeFormAction,
613 + );
614 + };
615 Object.defineProperties((proxy: any), {
578 - $$FORM_ACTION: {value: encodeFormAction},
616 + $$FORM_ACTION: {value: $$FORM_ACTION},
617 $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
618 bind: {value: bind},
619 });
@@ -587,7 +625,7 @@ export function registerServerReference(
625 const FunctionBind = Function.prototype.bind;
626 // $FlowFixMe[method-unbinding]
627 const ArraySlice = Array.prototype.slice;
590 -function bind(this: Function) {
628 +function bind(this: Function): Function {
629 // $FlowFixMe[unsupported-syntax]
630 const newFn = FunctionBind.apply(this, arguments);
631 const reference = knownServerReferences.get(this);
@@ -601,7 +639,17 @@ function bind(this: Function) {
639 } else {
640 boundPromise = Promise.resolve(args);
641 }
604 - registerServerReference(newFn, {id: reference.id, bound: boundPromise});
642 + // Expose encoder for use by SSR, as well as a special bind that can be used to
643 + // keep server capabilities.
644 + if (usedWithSSR) {
645 + // Only expose this in builds that would actually use it. Not needed on the client.
646 + Object.defineProperties((newFn: any), {
647 + $$FORM_ACTION: {value: this.$$FORM_ACTION},
648 + $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
649 + bind: {value: bind},
650 + });
651 + }
652 + knownServerReferences.set(newFn, {id: reference.id, bound: boundPromise});
653 }
654 return newFn;
655 }
@@ -609,12 +657,13 @@ function bind(this: Function) {
657 export function createServerReference<A: Iterable<any>, T>(
658 id: ServerReferenceId,
659 callServer: CallServerCallback,
660 + encodeFormAction?: EncodeFormActionCallback,
661 ): (...A) => Promise<T> {
662 const proxy = function (): Promise<T> {
663 // $FlowFixMe[method-unbinding]
664 const args = Array.prototype.slice.call(arguments);
665 return callServer(id, args);
666 };
618 - registerServerReference(proxy, {id, bound: null});
667 + registerServerReference(proxy, {id, bound: null}, encodeFormAction);
668 return proxy;
669 }
packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js
+1
@@ -38,6 +38,7 @@ function createResponseFromOptions(options: void | Options) {
38 options && options.moduleBaseURL ? options.moduleBaseURL : '',
39 null,
40 options && options.callServer ? options.callServer : undefined,
41 + undefined, // encodeFormAction
42 undefined, // nonce
43 );
44 }
packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js
+8 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes.js';
10 +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
11
12 import type {Response} from 'react-client/src/ReactFlightClient';
13
@@ -38,8 +38,14 @@ export function createServerReference<A: Iterable<any>, T>(
38 return createServerReferenceImpl(id, noServerCall);
39 }
40
41 +type EncodeFormActionCallback = <A>(
42 + id: any,
43 + args: Promise<A>,
44 +) => ReactCustomFormAction;
45 +
46 export type Options = {
47 nonce?: string,
48 + encodeFormAction?: EncodeFormActionCallback,
49 };
50
51 function createFromNodeStream<T>(
@@ -52,6 +58,7 @@ function createFromNodeStream<T>(
58 moduleRootPath,
59 moduleBaseURL,
60 noServerCall,
61 + options ? options.encodeFormAction : undefined,
62 options && typeof options.nonce === 'string' ? options.nonce : undefined,
63 );
64 stream.on('data', chunk => {
packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js
+1
@@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) {
37 null,
38 null,
39 options && options.callServer ? options.callServer : undefined,
40 + undefined, // encodeFormAction
41 undefined, // nonce
42 );
43 }
packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js
+8 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes.js';
10 +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
11
12 import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13
@@ -51,9 +51,15 @@ export function createServerReference<A: Iterable<any>, T>(
51 return createServerReferenceImpl(id, noServerCall);
52 }
53
54 +type EncodeFormActionCallback = <A>(
55 + id: any,
56 + args: Promise<A>,
57 +) => ReactCustomFormAction;
58 +
59 export type Options = {
60 ssrManifest: SSRManifest,
61 nonce?: string,
62 + encodeFormAction?: EncodeFormActionCallback,
63 };
64
65 function createResponseFromOptions(options: Options) {
@@ -61,6 +67,7 @@ function createResponseFromOptions(options: Options) {
67 options.ssrManifest.moduleMap,
68 options.ssrManifest.moduleLoading,
69 noServerCall,
70 + options.encodeFormAction,
71 typeof options.nonce === 'string' ? options.nonce : undefined,
72 );
73 }
packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js
+12 -4
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes.js';
10 +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
11
12 import type {Response} from 'react-client/src/ReactFlightClient';
13
@@ -40,9 +40,6 @@ function noServerCall() {
40 'to pass data to Client Components instead.',
41 );
42 }
43 -export type Options = {
44 - nonce?: string,
45 -};
43
44 export function createServerReference<A: Iterable<any>, T>(
45 id: any,
@@ -51,6 +48,16 @@ export function createServerReference<A: Iterable<any>, T>(
48 return createServerReferenceImpl(id, noServerCall);
49 }
50
51 +type EncodeFormActionCallback = <A>(
52 + id: any,
53 + args: Promise<A>,
54 +) => ReactCustomFormAction;
55 +
56 +export type Options = {
57 + nonce?: string,
58 + encodeFormAction?: EncodeFormActionCallback,
59 +};
60 +
61 function createFromNodeStream<T>(
62 stream: Readable,
63 ssrManifest: SSRManifest,
@@ -60,6 +67,7 @@ function createFromNodeStream<T>(
67 ssrManifest.moduleMap,
68 ssrManifest.moduleLoading,
69 noServerCall,
70 + options ? options.encodeFormAction : undefined,
71 options && typeof options.nonce === 'string' ? options.nonce : undefined,
72 );
73 stream.on('data', chunk => {
packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js
+1
@@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) {
37 null,
38 null,
39 options && options.callServer ? options.callServer : undefined,
40 + undefined, // encodeFormAction
41 undefined, // nonce
42 );
43 }
packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
+8 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes.js';
10 +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
11
12 import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13
@@ -51,9 +51,15 @@ export function createServerReference<A: Iterable<any>, T>(
51 return createServerReferenceImpl(id, noServerCall);
52 }
53
54 +type EncodeFormActionCallback = <A>(
55 + id: any,
56 + args: Promise<A>,
57 +) => ReactCustomFormAction;
58 +
59 export type Options = {
60 ssrManifest: SSRManifest,
61 nonce?: string,
62 + encodeFormAction?: EncodeFormActionCallback,
63 };
64
65 function createResponseFromOptions(options: Options) {
@@ -61,6 +67,7 @@ function createResponseFromOptions(options: Options) {
67 options.ssrManifest.moduleMap,
68 options.ssrManifest.moduleLoading,
69 noServerCall,
70 + options.encodeFormAction,
71 typeof options.nonce === 'string' ? options.nonce : undefined,
72 );
73 }
packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js
+8 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes.js';
10 +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
11
12 import type {Response} from 'react-client/src/ReactFlightClient';
13
@@ -48,8 +48,14 @@ export function createServerReference<A: Iterable<any>, T>(
48 return createServerReferenceImpl(id, noServerCall);
49 }
50
51 +type EncodeFormActionCallback = <A>(
52 + id: any,
53 + args: Promise<A>,
54 +) => ReactCustomFormAction;
55 +
56 export type Options = {
57 nonce?: string,
58 + encodeFormAction?: EncodeFormActionCallback,
59 };
60
61 function createFromNodeStream<T>(
@@ -61,6 +67,7 @@ function createFromNodeStream<T>(
67 ssrManifest.moduleMap,
68 ssrManifest.moduleLoading,
69 noServerCall,
70 + options ? options.encodeFormAction : undefined,
71 options && typeof options.nonce === 'string' ? options.nonce : undefined,
72 );
73 stream.on('data', chunk => {