From 2acfb7b60922bdc8376dd144ca7bc532df78254b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 5 Apr 2024 14:32:18 -0400 Subject: [PATCH] [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. --- .../react-client/src/ReactFlightClient.js | 10 ++++++ .../src/ReactFlightReplyClient.js | 2 +- .../src/__tests__/ReactFlight-test.js | 34 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 15 ++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 8a35e97b1679a..446c529c67ba2 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -746,6 +746,16 @@ function parseModelString( } return undefined; } + case 'K': { + // FormData + const id = parseInt(value.slice(2), 16); + const data = getOutlinedModel(response, id); + const formData = new FormData(); + for (let i = 0; i < data.length; i++) { + formData.append(data[i][0], data[i][1]); + } + return formData; + } case 'I': { // $Infinity return Infinity; diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index e3d19319c2f45..21fd5e565230b 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -75,7 +75,6 @@ export type ReactServerValue = | string | boolean | number - | symbol | null | void | bigint @@ -83,6 +82,7 @@ export type ReactServerValue = | Array | Map | Set + | FormData | Date | ReactServerObject | Promise; // Thenable diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 6fbd2360c82ef..263709a6321fc 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -468,6 +468,40 @@ describe('ReactFlight', () => { `); }); + if (typeof FormData !== 'undefined') { + it('can transport FormData (no blobs)', async () => { + function ComponentClient({prop}) { + return ` + formData: ${prop instanceof FormData} + hi: ${prop.get('hi')} + multiple: ${prop.getAll('multiple')} + content: ${JSON.stringify(Array.from(prop))} + `; + } + const Component = clientReference(ComponentClient); + + const formData = new FormData(); + formData.append('hi', 'world'); + formData.append('multiple', 1); + formData.append('multiple', 2); + + const model = ; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(` + formData: true + hi: world + multiple: 1,2 + content: [["hi","world"],["multiple","1"],["multiple","2"]] + `); + }); + } + it('can transport cyclic objects', async () => { function ComponentClient({prop}) { expect(prop.obj.obj.obj).toBe(prop.obj.obj); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 4c8fa75fec9e9..57079536b5758 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -239,6 +239,7 @@ export type ReactClientValue = | Array | Map | Set + | FormData | $ArrayBufferView | ArrayBuffer | Date @@ -1186,6 +1187,12 @@ function serializeMap( return '$Q' + id.toString(16); } +function serializeFormData(request: Request, formData: FormData): string { + const entries = Array.from(formData.entries()); + const id = outlineModel(request, (entries: any)); + return '$K' + id.toString(16); +} + function serializeSet(request: Request, set: Set): string { const entries = Array.from(set); for (let i = 0; i < entries.length; i++) { @@ -1595,6 +1602,10 @@ function renderModelDestructive( if (value instanceof Set) { return serializeSet(request, value); } + // TODO: FormData is not available in old Node. Remove the typeof later. + if (typeof FormData === 'function' && value instanceof FormData) { + return serializeFormData(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) { @@ -2139,6 +2150,10 @@ function renderConsoleValue( if (value instanceof Set) { return serializeSet(request, value); } + // TODO: FormData is not available in old Node. Remove the typeof later. + if (typeof FormData === 'function' && value instanceof FormData) { + return serializeFormData(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) {