Skip to content

Commit a7504ae

Browse files
refactor(router-core): simplify defaultParseSearch & defaultStringifySearch (#4985)
Wait for this PR before merging: #4987. It adds unit tests that should help make sure we're not breaking anything here. --- The functions used to serialize and deserialize search params are called *a lot*, but they are creating many unnecessary objects. This PR refactors - `encode` to be only as generic as what we need => in practice we only need to handle 1 value per key (i.e. `.set()` and not `.append()`) so we don't need a special case for arrays - `decode` can avoid creating many arrays - `stringifySearchWith` (and `defaultStringifySearch`) don't need to pre-stringify the input into a `<string,string>` dictionary as `encode` now accepts a stringifier function <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Breaking Changes - Removed encode and decode from React Router and Solid Router public exports. - Query string prefix support removed in encode/decode. - Refactor - Updated query string handling: arrays encode as comma-separated values; duplicate keys decode into arrays. - Streamlined error handling and avoided input mutation; consistent leading “?” management. - Tests - Updated tests to match new encoding/decoding behavior and removed prefix-related cases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent f1c13a0 commit a7504ae

File tree

5 files changed

+36
-60
lines changed

5 files changed

+36
-60
lines changed

packages/react-router/src/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ export {
1616
matchPathname,
1717
removeBasepath,
1818
matchByPath,
19-
encode,
20-
decode,
2119
rootRouteId,
2220
defaultSerializeError,
2321
defaultParseSearch,

packages/router-core/src/qss.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,36 @@
66
* This reimplementation uses modern browser APIs
77
* (namely URLSearchParams) and TypeScript while still
88
* maintaining the original functionality and interface.
9+
*
10+
* Update: this implementation has also been mangled to
11+
* fit exactly our use-case (single value per key in encoding).
912
*/
1013

1114
/**
1215
* Encodes an object into a query string.
1316
* @param obj - The object to encode into a query string.
14-
* @param [pfx] - An optional prefix to add before the query string.
17+
* @param stringify - An optional custom stringify function.
1518
* @returns The encoded query string.
1619
* @example
1720
* ```
1821
* // Example input: encode({ token: 'foo', key: 'value' })
1922
* // Expected output: "token=foo&key=value"
2023
* ```
2124
*/
22-
export function encode(obj: any, pfx?: string) {
23-
const normalizedObject = Object.entries(obj).flatMap(([key, value]) => {
24-
if (Array.isArray(value)) {
25-
return value.map((v) => [key, String(v)])
26-
} else {
27-
return [[key, String(value)]]
28-
}
29-
})
25+
export function encode(
26+
obj: Record<string, any>,
27+
stringify: (value: any) => string = String,
28+
): string {
29+
const result = new URLSearchParams()
3030

31-
const searchParams = new URLSearchParams(normalizedObject)
31+
for (const key in obj) {
32+
const val = obj[key]
33+
if (val !== undefined) {
34+
result.set(key, stringify(val))
35+
}
36+
}
3237

33-
return (pfx || '') + searchParams.toString()
38+
return result.toString()
3439
}
3540

3641
/**
@@ -52,28 +57,26 @@ function toValue(str: unknown) {
5257
/**
5358
* Decodes a query string into an object.
5459
* @param str - The query string to decode.
55-
* @param [pfx] - An optional prefix to filter out from the query string.
5660
* @returns The decoded key-value pairs in an object format.
5761
* @example
5862
* // Example input: decode("token=foo&key=value")
5963
* // Expected output: { "token": "foo", "key": "value" }
6064
*/
61-
export function decode(str: any, pfx?: string): any {
62-
const searchParamsPart = pfx ? str.slice(pfx.length) : str
63-
const searchParams = new URLSearchParams(searchParamsPart)
65+
export function decode(str: any): any {
66+
const searchParams = new URLSearchParams(str)
6467

65-
const entries = [...searchParams.entries()]
68+
const result: Record<string, unknown> = {}
6669

67-
return entries.reduce<Record<string, unknown>>((acc, [key, value]) => {
68-
const previousValue = acc[key]
70+
for (const [key, value] of searchParams.entries()) {
71+
const previousValue = result[key]
6972
if (previousValue == null) {
70-
acc[key] = toValue(value)
73+
result[key] = toValue(value)
74+
} else if (Array.isArray(previousValue)) {
75+
previousValue.push(toValue(value))
7176
} else {
72-
acc[key] = Array.isArray(previousValue)
73-
? [...previousValue, toValue(value)]
74-
: [previousValue, toValue(value)]
77+
result[key] = [previousValue, toValue(value)]
7578
}
79+
}
7680

77-
return acc
78-
}, {})
81+
return result
7982
}

packages/router-core/src/searchParams.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const defaultStringifySearch = stringifySearchWith(
99

1010
export function parseSearchWith(parser: (str: string) => any) {
1111
return (searchStr: string): AnySchema => {
12-
if (searchStr.substring(0, 1) === '?') {
12+
if (searchStr[0] === '?') {
1313
searchStr = searchStr.substring(1)
1414
}
1515

@@ -21,8 +21,8 @@ export function parseSearchWith(parser: (str: string) => any) {
2121
if (typeof value === 'string') {
2222
try {
2323
query[key] = parser(value)
24-
} catch (err) {
25-
//
24+
} catch (_err) {
25+
// silent
2626
}
2727
}
2828
}
@@ -35,40 +35,29 @@ export function stringifySearchWith(
3535
stringify: (search: any) => string,
3636
parser?: (str: string) => any,
3737
) {
38+
const hasParser = typeof parser === 'function'
3839
function stringifyValue(val: any) {
3940
if (typeof val === 'object' && val !== null) {
4041
try {
4142
return stringify(val)
42-
} catch (err) {
43+
} catch (_err) {
4344
// silent
4445
}
45-
} else if (typeof val === 'string' && typeof parser === 'function') {
46+
} else if (hasParser && typeof val === 'string') {
4647
try {
4748
// Check if it's a valid parseable string.
4849
// If it is, then stringify it again.
4950
parser(val)
5051
return stringify(val)
51-
} catch (err) {
52+
} catch (_err) {
5253
// silent
5354
}
5455
}
5556
return val
5657
}
5758

5859
return (search: Record<string, any>) => {
59-
search = { ...search }
60-
61-
Object.keys(search).forEach((key) => {
62-
const val = search[key]
63-
if (typeof val === 'undefined' || val === undefined) {
64-
delete search[key]
65-
} else {
66-
search[key] = stringifyValue(val)
67-
}
68-
})
69-
70-
const searchStr = encode(search as Record<string, string>).toString()
71-
60+
const searchStr = encode(search, stringifyValue)
7261
return searchStr ? `?${searchStr}` : ''
7362
}
7463
}

packages/router-core/tests/qss.test.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ describe('encode function', () => {
88
expect(queryString).toEqual('token=foo&key=value')
99
})
1010

11-
it('should encode an object into a query string with a prefix', () => {
12-
const obj = { token: 'foo', key: 'value' }
13-
const queryString = encode(obj, 'prefix_/*&?')
14-
expect(queryString).toEqual('prefix_/*&?token=foo&key=value')
15-
})
16-
1711
it('should handle encoding an object with empty values and trailing equal signs', () => {
1812
const obj = { token: '', key: 'value=' }
1913
const queryString = encode(obj)
@@ -23,7 +17,7 @@ describe('encode function', () => {
2317
it('should handle encoding an object with array values', () => {
2418
const obj = { token: ['foo', 'bar'], key: 'value' }
2519
const queryString = encode(obj)
26-
expect(queryString).toEqual('token=foo&token=bar&key=value')
20+
expect(queryString).toEqual('token=foo%2Cbar&key=value')
2721
})
2822

2923
it('should handle encoding an object with special characters', () => {
@@ -46,12 +40,6 @@ describe('decode function', () => {
4640
expect(decodedObj).toEqual({ token: 'foo', key: 'value' })
4741
})
4842

49-
it('should decode a query string with a prefix', () => {
50-
const queryString = 'prefix_/*&?token=foo&key=value'
51-
const decodedObj = decode(queryString, 'prefix_/*&?')
52-
expect(decodedObj).toEqual({ token: 'foo', key: 'value' })
53-
})
54-
5543
it('should handle missing values and trailing equal signs', () => {
5644
const queryString = 'token=&key=value='
5745
const decodedObj = decode(queryString)

packages/solid-router/src/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ export {
1616
matchPathname,
1717
removeBasepath,
1818
matchByPath,
19-
encode,
20-
decode,
2119
rootRouteId,
2220
defaultSerializeError,
2321
defaultParseSearch,

0 commit comments

Comments
 (0)