Skip to content

Commit

Permalink
[Flight] Support FormData from Server to Client (#28754)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage authored Apr 5, 2024
1 parent d1547de commit 2acfb7b
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 1 deletion.
10 changes: 10 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ export type ReactServerValue =
| string
| boolean
| number
| symbol
| null
| void
| bigint
| Iterable<ReactServerValue>
| Array<ReactServerValue>
| Map<ReactServerValue, ReactServerValue>
| Set<ReactServerValue>
| FormData
| Date
| ReactServerObject
| Promise<ReactServerValue>; // Thenable<ReactServerValue>
Expand Down
34 changes: 34 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Component prop={formData} />;

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);
Expand Down
15 changes: 15 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export type ReactClientValue =
| Array<ReactClientValue>
| Map<ReactClientValue, ReactClientValue>
| Set<ReactClientValue>
| FormData
| $ArrayBufferView
| ArrayBuffer
| Date
Expand Down Expand Up @@ -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<ReactClientValue>): string {
const entries = Array.from(set);
for (let i = 0; i < entries.length; i++) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 2acfb7b

Please sign in to comment.