Skip to content

Commit f61b19f

Browse files
committed
fix: Support custom stringifySearchWith in TanStack Router adapter (#1127)
1 parent e3016ce commit f61b19f

File tree

1 file changed

+69
-14
lines changed

1 file changed

+69
-14
lines changed

packages/nuqs/src/adapters/tanstack-router.ts

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
11
import {
22
stringifySearchWith,
3+
parseSearchWith,
34
useLocation,
45
useMatches,
5-
useNavigate
6+
useNavigate,
7+
type AnySchema
68
} from '@tanstack/react-router'
7-
import { startTransition, useCallback, useMemo } from 'react'
9+
import {
10+
createContext,
11+
createElement,
12+
startTransition,
13+
useCallback,
14+
useContext,
15+
useMemo,
16+
type ReactElement,
17+
type ReactNode
18+
} from 'react'
819
import { renderQueryString } from '../lib/url-encoding'
9-
import { createAdapterProvider, type AdapterProvider } from './lib/context'
20+
import { createAdapterProvider, type AdapterProps } from './lib/context'
1021
import type { AdapterInterface, UpdateUrlFunction } from './lib/defs'
1122

1223
// Use TanStack Router's default JSON-based search param serialization
24+
// The default behavior is compatible with nuqs' expected behavior
1325
const defaultStringifySearch = stringifySearchWith(JSON.stringify)
26+
const defaultParseSearch = parseSearchWith(JSON.parse)
27+
28+
type TanstackRouterAdapterContextType = {
29+
stringifySearchWith?: (search: Record<string, any>) => string
30+
}
31+
32+
const NuqsTanstackRouterAdapterContext =
33+
createContext<TanstackRouterAdapterContextType>({
34+
stringifySearchWith: undefined
35+
})
1436

1537
function useNuqsTanstackRouterAdapter(watchKeys: string[]): AdapterInterface {
38+
const { stringifySearchWith } = useContext(NuqsTanstackRouterAdapterContext)
39+
1640
const search = useLocation({
1741
select: state =>
1842
Object.fromEntries(
@@ -26,17 +50,33 @@ function useNuqsTanstackRouterAdapter(watchKeys: string[]): AdapterInterface {
2650
? (matches[matches.length - 1]?.fullPath as string)
2751
: undefined
2852
})
29-
const searchParams = useMemo(
30-
() =>
31-
// Use TSR’s default stringify to convert search object → URLSearchParams.
32-
// This avoids issues where arrays/objects were previously flattened
33-
// into invalid values like "[object Object]".
34-
new URLSearchParams(defaultStringifySearch(search)),
35-
[search, watchKeys.join(',')]
36-
)
53+
const searchParams = useMemo(() => {
54+
// Regardless of whether the user specified a custom parseSearchWith,
55+
// the search object here is already the result after parsing.
56+
// We use the default defaultStringifySearch to convert the search
57+
// to search params that nuqs can handle correctly.
58+
//
59+
// Use TSR's default stringify to convert search object → URLSearchParams.
60+
// This avoids issues where arrays/objects were previously flattened
61+
// into invalid values like "[object Object]".
62+
return new URLSearchParams(defaultStringifySearch(search))
63+
}, [search, watchKeys.join(',')])
3764

3865
const updateUrl: UpdateUrlFunction = useCallback(
3966
(search, options) => {
67+
let processedSearch: URLSearchParams
68+
if (stringifySearchWith) {
69+
// When updating, the search (URLSearchParams) here is in nuqs-generated format.
70+
// We first use defaultParseSearch to parse it into a search object,
71+
// then use the custom stringifySearchWith to convert it to a new URLSearchParams.
72+
const searchObject = defaultParseSearch(search.toString())
73+
const customQueryString = stringifySearchWith(searchObject)
74+
processedSearch = new URLSearchParams(customQueryString)
75+
} else {
76+
// Use default behavior which is compatible with nuqs' expected behavior
77+
processedSearch = search
78+
}
79+
4080
// Wrapping in a startTransition seems to be necessary
4181
// to support scroll restoration
4282
startTransition(() => {
@@ -52,7 +92,7 @@ function useNuqsTanstackRouterAdapter(watchKeys: string[]): AdapterInterface {
5292
// When we clear the search, passing an empty string causes
5393
// a type error and possible basepath issues, so we switch it to '.' instead.
5494
// See https://github.com/47ng/nuqs/pull/953#issuecomment-3003583471
55-
to: renderQueryString(search) || '.',
95+
to: renderQueryString(processedSearch) || '.',
5696
// `from` will be handled by tanstack router match resolver, code snippet:
5797
// https://github.com/TanStack/router/blob/5d940e2d8bdb12e213eede0abe8012855433ec4b/packages/react-router/src/link.tsx#L108-L112
5898
...(from ? { from } : {}),
@@ -62,7 +102,7 @@ function useNuqsTanstackRouterAdapter(watchKeys: string[]): AdapterInterface {
62102
})
63103
})
64104
},
65-
[navigate, from]
105+
[navigate, from, stringifySearchWith]
66106
)
67107

68108
return {
@@ -72,6 +112,21 @@ function useNuqsTanstackRouterAdapter(watchKeys: string[]): AdapterInterface {
72112
}
73113
}
74114

75-
export const NuqsAdapter: AdapterProvider = createAdapterProvider(
115+
const NuqsTanstackRouterAdapter = createAdapterProvider(
76116
useNuqsTanstackRouterAdapter
77117
)
118+
119+
export function NuqsAdapter({
120+
children,
121+
stringifySearchWith,
122+
...adapterProps
123+
}: AdapterProps & {
124+
children: ReactNode
125+
stringifySearchWith?: (search: Record<string, any>) => string
126+
}): ReactElement {
127+
return createElement(
128+
NuqsTanstackRouterAdapterContext.Provider,
129+
{ value: { stringifySearchWith } },
130+
createElement(NuqsTanstackRouterAdapter, { ...adapterProps, children })
131+
)
132+
}

0 commit comments

Comments
 (0)