Skip to content

Commit

Permalink
fix(start): serialize FormData for server function input (#3138)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere authored Jan 10, 2025
1 parent ab5a21b commit 058756a
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-2 border m-2 grid gap-2">
<h3>Serialize FormData Fn POST Call</h3>
<div className="overflow-y-auto">
It should return{' '}
<code>
<pre data-testid="expected-serialize-formdata-server-fn-result">
Hello, {testValues.name}! You are{' '}
{testValues.age + testValues.__adder} years old.
</pre>
</code>
</div>
<form
className="flex flex-col gap-2"
data-testid="serialize-formdata-form"
onSubmit={(evt) => {
evt.preventDefault()
const data = new FormData(evt.currentTarget)
greetUser({ data }).then(setFormDataResult)
}}
>
<input type="text" name="name" defaultValue={testValues.name} />
<input type="number" name="age" defaultValue={testValues.age} />
<button
type="submit"
data-testid="test-serialize-formdata-fn-calls-btn"
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Submit
</button>
</form>
<div className="overflow-y-auto">
<pre data-testid="serialize-formdata-form-response">
{JSON.stringify(formDataResult)}
</pre>
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions e2e/start/basic/app/routes/server-fns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,6 +16,7 @@ function RouteComponent() {
<ConsistentServerFnCalls />
<MultipartServerFnCall />
<AllowServerFnReturnNull />
<SerializeFormDataFnCall />
</>
)
}
20 changes: 20 additions & 0 deletions e2e/start/basic/tests/base.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
24 changes: 23 additions & 1 deletion packages/react-router/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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<string, any> = {}
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, TSerializable> = T extends TSerializable
Expand Down

0 comments on commit 058756a

Please sign in to comment.