diff --git a/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx b/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx
new file mode 100644
index 0000000000..c33ff93686
--- /dev/null
+++ b/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react'
+import { createServerFn } from '@tanstack/start'
+
+const testValues = {
+ name: 'Sean',
+ age: 25,
+ __adder: 1,
+}
+
+export const greetUser = createServerFn()
+ .validator((data: unknown) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+ const age = data.get('age')
+
+ if (!name || !age) {
+ throw new Error('Name and age are required')
+ }
+
+ return {
+ name: name.toString(),
+ age: parseInt(age.toString(), 10),
+ }
+ })
+ .handler(async ({ data: { name, age } }) => {
+ return `Hello, ${name}! You are ${age + testValues.__adder} years old.`
+ })
+
+// Usage
+export function SerializeFormDataFnCall() {
+ const [formDataResult, setFormDataResult] = React.useState({})
+
+ return (
+
+
Serialize FormData Fn POST Call
+
+ It should return{' '}
+
+
+ Hello, {testValues.name}! You are{' '}
+ {testValues.age + testValues.__adder} years old.
+
+
+
+
+
+
+ {JSON.stringify(formDataResult)}
+
+
+
+ )
+}
diff --git a/e2e/start/basic/app/routes/server-fns.tsx b/e2e/start/basic/app/routes/server-fns.tsx
index 21939570b9..b971473310 100644
--- a/e2e/start/basic/app/routes/server-fns.tsx
+++ b/e2e/start/basic/app/routes/server-fns.tsx
@@ -4,6 +4,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { ConsistentServerFnCalls } from './-server-fns/consistent-fn-calls'
import { MultipartServerFnCall } from './-server-fns/multipart-formdata-fn-call'
import { AllowServerFnReturnNull } from './-server-fns/allow-fn-return-null'
+import { SerializeFormDataFnCall } from './-server-fns/serialize-formdata-fn-call'
export const Route = createFileRoute('/server-fns')({
component: RouteComponent,
@@ -15,6 +16,7 @@ function RouteComponent() {
+
>
)
}
diff --git a/e2e/start/basic/tests/base.spec.ts b/e2e/start/basic/tests/base.spec.ts
index 3b6158ed09..1a4ac7646d 100644
--- a/e2e/start/basic/tests/base.spec.ts
+++ b/e2e/start/basic/tests/base.spec.ts
@@ -223,3 +223,23 @@ test('Server function can return null for GET and POST calls', async ({
page.getByTestId('allow_return_null_postFn-response'),
).toContainText(JSON.stringify(null))
})
+
+test('Server function can correctly send and receive FormData', async ({
+ page,
+}) => {
+ await page.goto('/server-fns')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-serialize-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-serialize-formdata-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(
+ page.getByTestId('serialize-formdata-form-response'),
+ ).toContainText(expected)
+})
diff --git a/packages/react-router/src/transformer.ts b/packages/react-router/src/transformer.ts
index f58f03bb59..3fcd66d221 100644
--- a/packages/react-router/src/transformer.ts
+++ b/packages/react-router/src/transformer.ts
@@ -33,7 +33,7 @@ export const defaultTransformer: RouterTransformer = {
return val
}),
encode: (value: any) => {
- // When encodign, dive first
+ // When encoding, dive first
if (Array.isArray(value)) {
return value.map((v) => defaultTransformer.encode(v))
}
@@ -125,6 +125,28 @@ const transformers = [
// From
(v) => Object.assign(new Error(v.message), v),
),
+ createTransformer(
+ // Key
+ 'formData',
+ // Check
+ (v) => v instanceof FormData,
+ // To
+ (v: FormData) => {
+ const entries: Record = {}
+ v.forEach((value, key) => {
+ entries[key] = value
+ })
+ return entries
+ },
+ // From
+ (v) => {
+ const formData = new FormData()
+ Object.entries(v).forEach(([key, value]) => {
+ formData.append(key, value as string | Blob)
+ })
+ return formData
+ },
+ ),
] as const
export type TransformerStringify = T extends TSerializable