Skip to content

Commit 81da35a

Browse files
committed
feat: process url search params middleware
1 parent 97390a5 commit 81da35a

6 files changed

Lines changed: 104 additions & 13 deletions

File tree

packages/nuqs/src/adapters/lib/context.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type AdapterProps = {
1616
defaultOptions?: Partial<
1717
Pick<Options, 'shallow' | 'clearOnDefault' | 'scroll' | 'limitUrlUpdates'>
1818
>
19+
processUrlSearchParams?: (search: URLSearchParams) => URLSearchParams
1920
}
2021

2122
export type AdapterContext = AdapterProps & {
@@ -59,10 +60,10 @@ export type AdapterProvider = (
5960
export function createAdapterProvider(
6061
useAdapter: UseAdapterHook
6162
): AdapterProvider {
62-
return ({ children, defaultOptions, ...props }) =>
63+
return ({ children, defaultOptions, processUrlSearchParams, ...props }) =>
6364
createElement(
6465
context.Provider,
65-
{ ...props, value: { useAdapter, defaultOptions } },
66+
{ ...props, value: { useAdapter, defaultOptions, processUrlSearchParams } },
6667
children
6768
)
6869
}
@@ -77,3 +78,6 @@ export function useAdapter(watchKeys: string[]): AdapterInterface {
7778

7879
export const useAdapterDefaultOptions = (): AdapterProps['defaultOptions'] =>
7980
useContext(context).defaultOptions
81+
82+
export const useAdapterProcessUrlSearchParams = (): AdapterProps['processUrlSearchParams'] =>
83+
useContext(context).processUrlSearchParams

packages/nuqs/src/adapters/testing.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type TestingAdapterProps = {
2929
export function NuqsTestingAdapter({
3030
resetUrlUpdateQueueOnMount = true,
3131
defaultOptions,
32+
processUrlSearchParams,
3233
...props
3334
}: TestingAdapterProps): ReactElement {
3435
if (resetUrlUpdateQueueOnMount) {
@@ -59,7 +60,7 @@ export function NuqsTestingAdapter({
5960
})
6061
return createElement(
6162
context.Provider,
62-
{ value: { useAdapter, defaultOptions } },
63+
{ value: { useAdapter, defaultOptions, processUrlSearchParams } },
6364
props.children
6465
)
6566
}

packages/nuqs/src/lib/queues/throttle.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,62 @@ describe('throttle: flush', () => {
305305
new Error('updateUrl error')
306306
)
307307
})
308+
it('should process url search params', async () => {
309+
const mockAdapter = createMockAdapter()
310+
const queue = new ThrottledQueue()
311+
queue.push({
312+
key: 'a',
313+
query: 'a',
314+
options: {},
315+
})
316+
const promise = queue.flush(mockAdapter, function (search) {
317+
const params = new URLSearchParams(search)
318+
params.set('b', 'b')
319+
return params
320+
})
321+
expect(queue.controller).not.toBeNull()
322+
vi.runAllTimers()
323+
await expect(promise).resolves.toEqual(new URLSearchParams('?a=a&b=b'))
324+
})
325+
describe('should process url search params', () => {
326+
it('should add new params', async () => {
327+
const mockAdapter = createMockAdapter()
328+
const queue = new ThrottledQueue()
329+
queue.push({
330+
key: 'a',
331+
query: 'a',
332+
options: {},
333+
})
334+
const promise = queue.flush(mockAdapter, (search) => {
335+
const params = new URLSearchParams(search)
336+
params.set('b', 'b')
337+
return params
338+
})
339+
expect(queue.controller).not.toBeNull()
340+
vi.runAllTimers()
341+
await expect(promise).resolves.toEqual(new URLSearchParams('?a=a&b=b'))
342+
})
343+
it('should sort params', async () => {
344+
const mockAdapter = createMockAdapter()
345+
const queue = new ThrottledQueue()
346+
queue.push({
347+
key: 'b',
348+
query: 'b',
349+
options: {},
350+
})
351+
queue.push({
352+
key: 'a',
353+
query: 'a',
354+
options: {},
355+
})
356+
const promise = queue.flush(mockAdapter, (search) => {
357+
const entries = Array.from(search.entries())
358+
entries.sort(([a], [b]) => a.localeCompare(b))
359+
return new URLSearchParams(entries)
360+
})
361+
expect(queue.controller).not.toBeNull()
362+
vi.runAllTimers()
363+
await expect(promise).resolves.toEqual(new URLSearchParams('?a=a&b=b'))
364+
})
365+
})
308366
})

packages/nuqs/src/lib/queues/throttle.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,14 @@ export class ThrottledQueue {
6969
return this.updateMap.get(key)
7070
}
7171

72-
flush({
73-
getSearchParamsSnapshot = getSearchParamsSnapshotFromLocation,
74-
rateLimitFactor = 1,
75-
...adapter
76-
}: UpdateQueueAdapterContext): Promise<URLSearchParams> {
72+
flush(
73+
{
74+
getSearchParamsSnapshot = getSearchParamsSnapshotFromLocation,
75+
rateLimitFactor = 1,
76+
...adapter
77+
}: UpdateQueueAdapterContext,
78+
processUrlSearchParams?: (search: URLSearchParams) => URLSearchParams
79+
): Promise<URLSearchParams> {
7780
this.controller ??= new AbortController()
7881
if (!Number.isFinite(this.timeMs)) {
7982
debug('[nuqs gtq] Skipping flush due to throttleMs=Infinity')
@@ -90,7 +93,7 @@ export class ThrottledQueue {
9093
...adapter,
9194
autoResetQueueOnUpdate: adapter.autoResetQueueOnUpdate ?? true,
9295
getSearchParamsSnapshot
93-
})
96+
}, processUrlSearchParams)
9497
if (error === null) {
9598
this.resolvers!.resolve(search)
9699
} else {
@@ -152,10 +155,11 @@ export class ThrottledQueue {
152155
}
153156

154157
applyPendingUpdates(
155-
adapter: Required<Omit<UpdateQueueAdapterContext, 'rateLimitFactor'>>
158+
adapter: Required<Omit<UpdateQueueAdapterContext, 'rateLimitFactor'>>,
159+
processUrlSearchParams?: (search: URLSearchParams) => URLSearchParams
156160
): [URLSearchParams, null | unknown] {
157161
const { updateUrl, getSearchParamsSnapshot } = adapter
158-
const search = getSearchParamsSnapshot()
162+
let search = getSearchParamsSnapshot()
159163
debug(
160164
`[nuqs gtq] Applying %d pending update(s) on top of %s`,
161165
this.updateMap.size,
@@ -181,6 +185,9 @@ export class ThrottledQueue {
181185
search.set(key, value)
182186
}
183187
}
188+
if (processUrlSearchParams) {
189+
search = processUrlSearchParams(search)
190+
}
184191
try {
185192
compose(transitions, () => {
186193
updateUrl(search, options)

packages/nuqs/src/useQueryStates.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,3 +748,23 @@ describe('useQueryStates: adapter defaults', () => {
748748
expect(onUrlUpdate.mock.calls[0]![0].queryString).toBe('?test=pass')
749749
})
750750
})
751+
752+
describe('useQueryStates: process url search params', () => {
753+
it('should use adapter processUrlSearchParams when provided', async () => {
754+
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
755+
const useTestHook = () => useQueryStates({ test: parseAsString })
756+
const { result } = renderHook(useTestHook, {
757+
wrapper: withNuqsTestingAdapter({
758+
processUrlSearchParams: (search) => {
759+
const params = new URLSearchParams(search)
760+
params.set('test', 'processed')
761+
return params
762+
},
763+
onUrlUpdate
764+
})
765+
})
766+
await act(() => result.current[1]({ test: 'update' }))
767+
expect(onUrlUpdate).toHaveBeenCalledOnce()
768+
expect(onUrlUpdate.mock.calls[0]![0].queryString).toBe('?test=processed')
769+
})
770+
})

packages/nuqs/src/useQueryStates.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
2-
import { useAdapter, useAdapterDefaultOptions } from './adapters/lib/context'
2+
import { useAdapter, useAdapterDefaultOptions, useAdapterProcessUrlSearchParams } from './adapters/lib/context'
33
import type { Nullable, Options, UrlKeys } from './defs'
44
import { debug } from './lib/debug'
55
import { error } from './lib/errors'
@@ -69,6 +69,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
6969
): UseQueryStatesReturn<KeyMap> {
7070
const hookId = useId()
7171
const defaultOptions = useAdapterDefaultOptions()
72+
const processUrlSearchParams = useAdapterProcessUrlSearchParams()
7273

7374
const {
7475
history = 'replace',
@@ -333,7 +334,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
333334
// debounced update that will resolve afterwards.
334335
const globalPromise = debounceAborts.reduce(
335336
(previous, fn) => fn(previous),
336-
globalThrottleQueue.flush(adapter)
337+
globalThrottleQueue.flush(adapter, processUrlSearchParams)
337338
)
338339
return returnedPromise ?? globalPromise
339340
},

0 commit comments

Comments
 (0)