Skip to content

Commit ad0ad5e

Browse files
sebmarkbagekassens
authored andcommitted
[Flight] Support FormData from Server to Client (facebook#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. facebook#28755 adds support for Blobs when the `enableBinaryFlight` is enabled which allows them to be used inside FormData too.
1 parent c0b5d43 commit ad0ad5e

File tree

4 files changed

+60
-1
lines changed

4 files changed

+60
-1
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,16 @@ function parseModelString(
734734
}
735735
return undefined;
736736
}
737+
case 'K': {
738+
// FormData
739+
const id = parseInt(value.slice(2), 16);
740+
const data = getOutlinedModel(response, id);
741+
const formData = new FormData();
742+
for (let i = 0; i < data.length; i++) {
743+
formData.append(data[i][0], data[i][1]);
744+
}
745+
return formData;
746+
}
737747
case 'I': {
738748
// $Infinity
739749
return Infinity;

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 34 additions & 0 deletions
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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export type ReactClientValue =
237237
| Array<ReactClientValue>
238238
| Map<ReactClientValue, ReactClientValue>
239239
| Set<ReactClientValue>
240+
| FormData
240241
| $ArrayBufferView
241242
| ArrayBuffer
242243
| Date
@@ -1140,6 +1141,12 @@ function serializeMap(
11401141
return '$Q' + id.toString(16);
11411142
}
11421143

1144+
function serializeFormData(request: Request, formData: FormData): string {
1145+
const entries = Array.from(formData.entries());
1146+
const id = outlineModel(request, (entries: any));
1147+
return '$K' + id.toString(16);
1148+
}
1149+
11431150
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
11441151
const entries = Array.from(set);
11451152
for (let i = 0; i < entries.length; i++) {
@@ -1548,6 +1555,10 @@ function renderModelDestructive(
15481555
if (value instanceof Set) {
15491556
return serializeSet(request, value);
15501557
}
1558+
// TODO: FormData is not available in old Node. Remove the typeof later.
1559+
if (typeof FormData === 'function' && value instanceof FormData) {
1560+
return serializeFormData(request, value);
1561+
}
15511562

15521563
if (enableBinaryFlight) {
15531564
if (value instanceof ArrayBuffer) {
@@ -2073,6 +2084,10 @@ function renderConsoleValue(
20732084
if (value instanceof Set) {
20742085
return serializeSet(request, value);
20752086
}
2087+
// TODO: FormData is not available in old Node. Remove the typeof later.
2088+
if (typeof FormData === 'function' && value instanceof FormData) {
2089+
return serializeFormData(request, value);
2090+
}
20762091

20772092
if (enableBinaryFlight) {
20782093
if (value instanceof ArrayBuffer) {

0 commit comments

Comments
 (0)