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) {