@samitouri / QOS-React / commits / 2acfb7b609

[Flight] Support FormData from Server to Client (#28754)

We currently support FormData for Replies mainly for Form Actions. This supports it in the other direction too which lets you return it from an action as the response. Mainly for parity. We don't really recommend that you just pass the original form data back because the action is supposed to be able to clear fields and such but you could potentially at least use this as the format and could clear some fields. We could potentially optimize this with a temporary reference if the same object was passed to a reply in case you use it as a round trip to avoid serializing it back again. That way the action has the ability to override it to clear fields but if it doesn't you get back the same as you sent. #28755 adds support for Blobs when the `enableBinaryFlight` is enabled which allows them to be used inside FormData too.

Sebastian Markbåge committed Apr 5, 2024 at 14:32 UTC 2acfb7b60922bdc8376dd144ca7bc532df78254b
4 files changed +60 -1
packages/react-client/src/ReactFlightClient.js
+10
@@ -746,6 +746,16 @@ function parseModelString(
746 }
747 return undefined;
748 }
749 + case 'K': {
750 + // FormData
751 + const id = parseInt(value.slice(2), 16);
752 + const data = getOutlinedModel(response, id);
753 + const formData = new FormData();
754 + for (let i = 0; i < data.length; i++) {
755 + formData.append(data[i][0], data[i][1]);
756 + }
757 + return formData;
758 + }
759 case 'I': {
760 // $Infinity
761 return Infinity;
packages/react-client/src/ReactFlightReplyClient.js
+1 -1
@@ -75,7 +75,6 @@ export type ReactServerValue =
75 | string
76 | boolean
77 | number
78 - | symbol
78 | null
79 | void
80 | bigint
@@ -83,6 +82,7 @@ export type ReactServerValue =
82 | Array<ReactServerValue>
83 | Map<ReactServerValue, ReactServerValue>
84 | Set<ReactServerValue>
85 + | FormData
86 | Date
87 | ReactServerObject
88 | Promise<ReactServerValue>; // Thenable<ReactServerValue>
packages/react-client/src/__tests__/ReactFlight-test.js
+34
@@ -468,6 +468,40 @@ describe('ReactFlight', () => {
468 `);
469 });
470
471 + if (typeof FormData !== 'undefined') {
472 + it('can transport FormData (no blobs)', async () => {
473 + function ComponentClient({prop}) {
474 + return `
475 + formData: ${prop instanceof FormData}
476 + hi: ${prop.get('hi')}
477 + multiple: ${prop.getAll('multiple')}
478 + content: ${JSON.stringify(Array.from(prop))}
479 + `;
480 + }
481 + const Component = clientReference(ComponentClient);
482 +
483 + const formData = new FormData();
484 + formData.append('hi', 'world');
485 + formData.append('multiple', 1);
486 + formData.append('multiple', 2);
487 +
488 + const model = <Component prop={formData} />;
489 +
490 + const transport = ReactNoopFlightServer.render(model);
491 +
492 + await act(async () => {
493 + ReactNoop.render(await ReactNoopFlightClient.read(transport));
494 + });
495 +
496 + expect(ReactNoop).toMatchRenderedOutput(`
497 + formData: true
498 + hi: world
499 + multiple: 1,2
500 + content: [["hi","world"],["multiple","1"],["multiple","2"]]
501 + `);
502 + });
503 + }
504 +
505 it('can transport cyclic objects', async () => {
506 function ComponentClient({prop}) {
507 expect(prop.obj.obj.obj).toBe(prop.obj.obj);
packages/react-server/src/ReactFlightServer.js
+15
@@ -239,6 +239,7 @@ export type ReactClientValue =
239 | Array<ReactClientValue>
240 | Map<ReactClientValue, ReactClientValue>
241 | Set<ReactClientValue>
242 + | FormData
243 | $ArrayBufferView
244 | ArrayBuffer
245 | Date
@@ -1186,6 +1187,12 @@ function serializeMap(
1187 return '$Q' + id.toString(16);
1188 }
1189
1190 +function serializeFormData(request: Request, formData: FormData): string {
1191 + const entries = Array.from(formData.entries());
1192 + const id = outlineModel(request, (entries: any));
1193 + return '$K' + id.toString(16);
1194 +}
1195 +
1196 function serializeSet(request: Request, set: Set<ReactClientValue>): string {
1197 const entries = Array.from(set);
1198 for (let i = 0; i < entries.length; i++) {
@@ -1595,6 +1602,10 @@ function renderModelDestructive(
1602 if (value instanceof Set) {
1603 return serializeSet(request, value);
1604 }
1605 + // TODO: FormData is not available in old Node. Remove the typeof later.
1606 + if (typeof FormData === 'function' && value instanceof FormData) {
1607 + return serializeFormData(request, value);
1608 + }
1609
1610 if (enableBinaryFlight) {
1611 if (value instanceof ArrayBuffer) {
@@ -2139,6 +2150,10 @@ function renderConsoleValue(
2150 if (value instanceof Set) {
2151 return serializeSet(request, value);
2152 }
2153 + // TODO: FormData is not available in old Node. Remove the typeof later.
2154 + if (typeof FormData === 'function' && value instanceof FormData) {
2155 + return serializeFormData(request, value);
2156 + }
2157
2158 if (enableBinaryFlight) {
2159 if (value instanceof ArrayBuffer) {