Skip to content

Commit

Permalink
fix(start): preserve multiple values for same key in FormData seria…
Browse files Browse the repository at this point in the history
…lization (#3140)

Co-authored-by: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
  • Loading branch information
EskiMojo14 and SeanCassiere authored Jan 12, 2025
1 parent 13c750b commit 72fbd4c
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,32 @@ import { createServerFn } from '@tanstack/start'
const testValues = {
name: 'Sean',
age: 25,
pet1: 'dog',
pet2: 'cat',
__adder: 1,
}

export const greetUser = createServerFn()
.validator((data: unknown) => {
.validator((data: FormData) => {
if (!(data instanceof FormData)) {
throw new Error('Invalid! FormData is required')
}
const name = data.get('name')
const age = data.get('age')
const pets = data.getAll('pet')

if (!name || !age) {
throw new Error('Name and age are required')
if (!name || !age || pets.length === 0) {
throw new Error('Name, age and pets are required')
}

return {
name: name.toString(),
age: parseInt(age.toString(), 10),
pets: pets.map((pet) => pet.toString()),
}
})
.handler(async ({ data: { name, age } }) => {
return `Hello, ${name}! You are ${age + testValues.__adder} years old.`
.handler(({ data: { name, age, pets } }) => {
return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.`
})

// Usage
Expand All @@ -40,7 +44,8 @@ export function SerializeFormDataFnCall() {
<code>
<pre data-testid="expected-serialize-formdata-server-fn-result">
Hello, {testValues.name}! You are{' '}
{testValues.age + testValues.__adder} years old.
{testValues.age + testValues.__adder} years old, and your favorite{' '}
pets are {testValues.pet1},{testValues.pet2}.
</pre>
</code>
</div>
Expand All @@ -55,6 +60,8 @@ export function SerializeFormDataFnCall() {
>
<input type="text" name="name" defaultValue={testValues.name} />
<input type="number" name="age" defaultValue={testValues.age} />
<input type="text" name="pet" defaultValue={testValues.pet1} />
<input type="text" name="pet" defaultValue={testValues.pet2} />
<button
type="submit"
data-testid="test-serialize-formdata-fn-calls-btn"
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export type {
RouterTransformer,
TransformerParse,
TransformerStringify,
DefaultSerializable,
DefaultTransformerParse,
DefaultTransformerStringify,
} from './transformer'
Expand Down
52 changes: 37 additions & 15 deletions packages/react-router/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ export const defaultTransformer: RouterTransformer = {
},
}

const createTransformer = <T extends string>(
key: T,
check: (value: any) => boolean,
toValue: (value: any) => any = (v) => v,
fromValue: (value: any) => any = (v) => v,
const createTransformer = <TKey extends string, TInput, TSerialized>(
key: TKey,
check: (value: any) => value is TInput,
toValue: (value: TInput) => TSerialized,
fromValue: (value: TSerialized) => TInput,
) => ({
key,
stringifyCondition: check,
Expand All @@ -94,13 +94,14 @@ const createTransformer = <T extends string>(
})

// Keep these ordered by predicted frequency
// Make sure to keep DefaultSerializeable in sync with these transformers
// Also, make sure that they are unit tested in transformer.test.tsx
const transformers = [
createTransformer(
// Key
'undefined',
// Check
(v) => v === undefined,
(v): v is undefined => v === undefined,
// To
() => 0,
// From
Expand All @@ -110,7 +111,7 @@ const transformers = [
// Key
'date',
// Check
(v) => v instanceof Date,
(v): v is Date => v instanceof Date,
// To
(v) => v.toISOString(),
// From
Expand All @@ -120,7 +121,7 @@ const transformers = [
// Key
'error',
// Check
(v) => v instanceof Error,
(v): v is Error => v instanceof Error,
// To
(v) => ({ ...v, message: v.message, stack: v.stack, cause: v.cause }),
// From
Expand All @@ -130,20 +131,36 @@ const transformers = [
// Key
'formData',
// Check
(v) => v instanceof FormData,
(v): v is FormData => v instanceof FormData,
// To
(v: FormData) => {
const entries: Record<string, any> = {}
(v) => {
const entries: Record<
string,
Array<FormDataEntryValue> | FormDataEntryValue
> = {}
v.forEach((value, key) => {
entries[key] = value
const entry = entries[key]
if (entry !== undefined) {
if (Array.isArray(entry)) {
entry.push(value)
} else {
entries[key] = [entry, value]
}
} else {
entries[key] = value
}
})
return entries
},
// From
(v) => {
const formData = new FormData()
Object.entries(v).forEach(([key, value]) => {
formData.append(key, value as string | Blob)
if (Array.isArray(value)) {
value.forEach((val) => formData.append(key, val))
} else {
formData.append(key, value)
}
})
return formData
},
Expand All @@ -162,9 +179,14 @@ export type TransformerParse<T, TSerializable> = T extends TSerializable
? ReadableStream
: { [K in keyof T]: TransformerParse<T[K], TSerializable> }

export type DefaultSerializable = Date | undefined | Error | FormData

export type DefaultTransformerStringify<T> = TransformerStringify<
T,
Date | undefined
DefaultSerializable
>

export type DefaultTransformerParse<T> = TransformerParse<T, Date | undefined>
export type DefaultTransformerParse<T> = TransformerParse<
T,
DefaultSerializable
>
20 changes: 20 additions & 0 deletions packages/react-router/tests/transformer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ describe('transformer.stringify', () => {
`"{"$formData":{"foo":"bar","name":"Sean"}}"`,
)
})
it('should stringify FormData with multiple values for the same key', () => {
const formData = new FormData()
formData.append('foo', 'bar')
formData.append('foo', 'baz')
expect(tf.stringify(formData)).toMatchInlineSnapshot(
`"{"$formData":{"foo":["bar","baz"]}}"`,
)
})
})

describe('transformer.parse', () => {
Expand Down Expand Up @@ -99,4 +107,16 @@ describe('transformer.parse', () => {
['name', 'Sean'],
])
})
it('should parse FormData with multiple values for the same key', () => {
const formData = new FormData()
formData.append('foo', 'bar')
formData.append('foo', 'baz')
const str = tf.stringify(formData)
const parsed = tf.parse(str) as FormData
expect(parsed).toBeInstanceOf(FormData)
expect([...parsed.entries()]).toEqual([
['foo', 'bar'],
['foo', 'baz'],
])
})
})

0 comments on commit 72fbd4c

Please sign in to comment.