Skip to content

Commit 2acfb7b

Browse files
authored
[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.
1 parent d1547de commit 2acfb7b

File tree

4 files changed

+60
-1
lines changed

4 files changed

+60
-1
lines changed

packages/react-client/src/ReactFlightClient.js

+10
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,16 @@ function parseModelString(
746746
}
747747
return undefined;
748748
}
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+
}
749759
case 'I': {
750760
// $Infinity
751761
return Infinity;

packages/react-client/src/ReactFlightReplyClient.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ export type ReactServerValue =
7575
| string
7676
| boolean
7777
| number
78-
| symbol
7978
| null
8079
| void
8180
| bigint
8281
| Iterable<ReactServerValue>
8382
| Array<ReactServerValue>
8483
| Map<ReactServerValue, ReactServerValue>
8584
| Set<ReactServerValue>
85+
| FormData
8686
| Date
8787
| ReactServerObject
8888
| Promise<ReactServerValue>; // Thenable<ReactServerValue>

packages/react-client/src/__tests__/ReactFlight-test.js

+34
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,40 @@ describe('ReactFlight', () => {
468468
`);
469469
});
470470

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+
471505
it('can transport cyclic objects', async () => {
472506
function ComponentClient({prop}) {
473507
expect(prop.obj.obj.obj).toBe(prop.obj.obj);

packages/react-server/src/ReactFlightServer.js

+15
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export type ReactClientValue =
239239
| Array<ReactClientValue>
240240
| Map<ReactClientValue, ReactClientValue>
241241
| Set<ReactClientValue>
242+
| FormData
242243
| $ArrayBufferView
243244
| ArrayBuffer
244245
| Date
@@ -1186,6 +1187,12 @@ function serializeMap(
11861187
return '$Q' + id.toString(16);
11871188
}
11881189

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+
11891196
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
11901197
const entries = Array.from(set);
11911198
for (let i = 0; i < entries.length; i++) {
@@ -1595,6 +1602,10 @@ function renderModelDestructive(
15951602
if (value instanceof Set) {
15961603
return serializeSet(request, value);
15971604
}
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+
}
15981609

15991610
if (enableBinaryFlight) {
16001611
if (value instanceof ArrayBuffer) {
@@ -2139,6 +2150,10 @@ function renderConsoleValue(
21392150
if (value instanceof Set) {
21402151
return serializeSet(request, value);
21412152
}
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+
}
21422157

21432158
if (enableBinaryFlight) {
21442159
if (value instanceof ArrayBuffer) {

0 commit comments

Comments
 (0)