Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/search-param-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/router-core': minor
---

Pass the param's key as a second argument to the `parseSearchWith` and `stringifySearchWith` callbacks, so search params can be parsed and serialized per key.
20 changes: 15 additions & 5 deletions docs/router/guide/custom-search-param-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ It would be serialized and escaped into the following search string:
?page=1&sort=asc&filters=%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D
```

We can implement the default behavior with the following code:
We can override the behavior for specific parameters using the `key` argument passed to each callback (here keeping `q` as a literal string while everything else uses the default `JSON` behavior):

<!-- ::start:framework -->

Expand All @@ -35,8 +35,13 @@ import {

const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
parseSearch: parseSearchWith((value, key) =>
key === 'q' ? value : JSON.parse(value),
),
stringifySearch: stringifySearchWith(
(value, key) => (key === 'q' ? String(value) : JSON.stringify(value)),
(value, key) => JSON.parse(value),
),
})
```

Expand All @@ -51,8 +56,13 @@ import {

const router = createRouter({
// ...
parseSearch: parseSearchWith(JSON.parse),
stringifySearch: stringifySearchWith(JSON.stringify),
parseSearch: parseSearchWith((value, key) =>
key === 'q' ? value : JSON.parse(value),
),
stringifySearch: stringifySearchWith(
(value, key) => (key === 'q' ? String(value) : JSON.stringify(value)),
(value, key) => JSON.parse(value),
),
})
```

Expand Down
4 changes: 2 additions & 2 deletions packages/router-core/src/qss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
*/
export function encode(
obj: Record<string, any>,
stringify: (value: any) => string = String,
stringify: (value: any, key: string) => string = String,
): string {
const result = new URLSearchParams()

for (const key in obj) {
const val = obj[key]
if (val !== undefined) {
result.set(key, stringify(val))
result.set(key, stringify(val, key))
}
}

Expand Down
22 changes: 11 additions & 11 deletions packages/router-core/src/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { decode, encode } from './qss'
import type { AnySchema } from './validators'

/** Default `parseSearch` that strips leading '?' and JSON-parses values. */
export const defaultParseSearch = parseSearchWith(JSON.parse)
export const defaultParseSearch = parseSearchWith((value) => JSON.parse(value))
/** Default `stringifySearch` using JSON.stringify for complex values. */
export const defaultStringifySearch = stringifySearchWith(
JSON.stringify,
JSON.parse,
(value) => JSON.stringify(value),
(value) => JSON.parse(value),
)

/**
Expand All @@ -19,7 +19,7 @@ export const defaultStringifySearch = stringifySearchWith(
* @returns A `parseSearch` function compatible with `Router` options.
* @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization
*/
export function parseSearchWith(parser: (str: string) => any) {
export function parseSearchWith(parser: (str: string, key: string) => any) {
return (searchStr: string): AnySchema => {
if (searchStr[0] === '?') {
searchStr = searchStr.substring(1)
Expand All @@ -32,7 +32,7 @@ export function parseSearchWith(parser: (str: string) => any) {
const value = query[key]
if (typeof value === 'string') {
try {
query[key] = parser(value)
query[key] = parser(value, key)
} catch (_err) {
// silent
}
Expand All @@ -56,23 +56,23 @@ export function parseSearchWith(parser: (str: string) => any) {
* @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization
*/
export function stringifySearchWith(
stringify: (search: any) => string,
parser?: (str: string) => any,
stringify: (search: any, key: string) => string,
parser?: (str: string, key: string) => any,
) {
const hasParser = typeof parser === 'function'
function stringifyValue(val: any) {
function stringifyValue(val: any, key: string) {
if (typeof val === 'object' && val !== null) {
try {
return stringify(val)
return stringify(val, key)
} catch (_err) {
// silent
}
} else if (hasParser && typeof val === 'string') {
try {
// Check if it's a valid parseable string.
// If it is, then stringify it again.
parser(val)
return stringify(val)
parser(val, key)
return stringify(val, key)
} catch (_err) {
// silent
}
Expand Down
7 changes: 7 additions & 0 deletions packages/router-core/tests/qss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ describe('encode function', () => {
const queryString = encode(obj)
expect(queryString).toEqual('foo%3Dbar=1')
})

it('should pass the key to the stringify callback', () => {
const queryString = encode({ a: 'x', b: 'x' }, (value, key) =>
key === 'a' ? String(value) : JSON.stringify(value),
)
expect(queryString).toEqual('a=x&b=%22x%22')
})
})

describe('decode function', () => {
Expand Down
23 changes: 22 additions & 1 deletion packages/router-core/tests/searchParams.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, test } from 'vitest'
import { defaultParseSearch, defaultStringifySearch } from '../src'
import {
defaultParseSearch,
defaultStringifySearch,
parseSearchWith,
stringifySearchWith,
} from '../src'

describe('Search Params serialization and deserialization', () => {
/*
Expand Down Expand Up @@ -98,4 +103,20 @@ describe('Search Params serialization and deserialization', () => {
'?foo=%222024-11-18T00%3A00%3A00.000Z%22',
)
})

test('serializes a chosen key differently from the rest', () => {
// Keep `q` as a literal string while everything else uses JSON.
const parse = parseSearchWith((value, key) =>
key === 'q' ? value : JSON.parse(value),
)
const stringify = stringifySearchWith(
(value, key) => (key === 'q' ? String(value) : JSON.stringify(value)),
(value, key) => JSON.parse(value),
)

const search = { q: '[1,2,3]', filters: { a: 1 } }
const str = stringify(search)
expect(str).toEqual('?q=%5B1%2C2%2C3%5D&filters=%7B%22a%22%3A1%7D')
expect(parse(str)).toEqual(search)
})
})