Skip to content

Commit 099ceb3

Browse files
authored
fix: Allow parser-level options in useQueryStates (#621)
* fix: Allow parser-level options in useQueryStates Order of precedence (first non-nullish wins): - call level - parser level - hook-global level (otherwise default). Also fixes a bug with `clearOnDefault` in useQueryState where a `true` top-level value could not be overriden by a call-level `false` value. Closes #618.
1 parent e2c88a6 commit 099ceb3

File tree

6 files changed

+108
-19
lines changed

6 files changed

+108
-19
lines changed

packages/docs/content/docs/batching.mdx

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: useQueryStates
33
description: How to read & update multiple search params at once
44
---
55

6-
## Multiple Queries (batching)
6+
## Multiple updates (batching)
77

88
You can call as many state update function as needed in a single event loop
99
tick, and they will be applied to the URL asynchronously:
@@ -80,3 +80,21 @@ const search = await setCoordinates({
8080
lng: Math.random() * 360 - 180
8181
})
8282
```
83+
84+
### Options
85+
86+
There are three places you can define [options](./options) in `useQueryStates`:
87+
- As the second argument to the hook itself (global options, see above)
88+
- On each parser, like `parseAsFloat.withOptions({ shallow: false }){:ts}`
89+
- At the call level when updating the state:
90+
91+
```ts
92+
setCoordinates({
93+
lat: 42,
94+
lng: 12
95+
}, {
96+
shallow: false
97+
})
98+
```
99+
100+
The order of precedence is: call-level options > parser options > global options.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference types="cypress" />
2+
3+
it('useQueryStates options', () => {
4+
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
5+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
6+
cy.get('#1').click()
7+
cy.location('search').should('eq', '?b=')
8+
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
9+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
10+
cy.get('#2').click()
11+
cy.location('search').should('eq', '?a=&b=')
12+
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
13+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
14+
cy.get('#3').click()
15+
cy.location('search').should('eq', '')
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use client'
2+
3+
import { parseAsString, useQueryStates } from 'nuqs'
4+
import { Suspense } from 'react'
5+
6+
export default function Page() {
7+
return (
8+
<Suspense>
9+
<Client />
10+
</Suspense>
11+
)
12+
}
13+
14+
function Client() {
15+
const [values, setValues] = useQueryStates(
16+
{
17+
a: parseAsString.withDefault(''),
18+
b: parseAsString.withDefault('').withOptions({
19+
clearOnDefault: false
20+
})
21+
},
22+
{
23+
clearOnDefault: true
24+
}
25+
)
26+
return (
27+
<>
28+
<button id="1" onClick={() => setValues({ a: '', b: '' })}>
29+
1
30+
</button>
31+
<button
32+
id="2"
33+
onClick={() => setValues({ a: '', b: '' }, { clearOnDefault: false })}
34+
>
35+
2
36+
</button>
37+
<button
38+
id="3"
39+
onClick={() => setValues({ a: '', b: '' }, { clearOnDefault: true })}
40+
>
41+
3
42+
</button>
43+
</>
44+
)
45+
}

packages/nuqs/src/update-queue.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ export function enqueueQueryStringUpdate<Value>(
2727
key: string,
2828
value: Value | null,
2929
serialize: (value: Value) => string,
30-
options: Options
30+
options: Pick<
31+
Options,
32+
'history' | 'scroll' | 'shallow' | 'startTransition' | 'throttleMs'
33+
>
3134
) {
3235
const serializedOrNull = value === null ? null : serialize(value)
3336
debug('[nuqs queue] Enqueueing %s=%s %O', key, serializedOrNull, options)

packages/nuqs/src/useQueryState.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export function useQueryState<T = string>(
281281
? stateUpdater(stateRef.current ?? defaultValue ?? null)
282282
: stateUpdater
283283
if (
284-
(options.clearOnDefault || clearOnDefault) &&
284+
(options.clearOnDefault ?? clearOnDefault) &&
285285
newValue !== null &&
286286
defaultValue !== undefined &&
287287
eq(newValue, defaultValue)

packages/nuqs/src/useQueryStates.ts

+23-16
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {
1616
} from './update-queue'
1717
import { safeParse } from './utils'
1818

19-
type KeyMapValue<Type> = Parser<Type> & {
20-
defaultValue?: Type
21-
}
19+
type KeyMapValue<Type> = Parser<Type> &
20+
Options & {
21+
defaultValue?: Type
22+
}
2223

2324
export type UseQueryStatesKeysMap<Map = any> = {
2425
[Key in keyof Map]: KeyMapValue<Map[Key]>
@@ -135,33 +136,39 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
135136
}, [keyMap])
136137

137138
const update = React.useCallback<SetValues<KeyMap>>(
138-
(stateUpdater, options = {}) => {
139+
(stateUpdater, callOptions = {}) => {
139140
const newState: Partial<Nullable<KeyMap>> =
140141
typeof stateUpdater === 'function'
141142
? stateUpdater(stateRef.current)
142143
: stateUpdater
143144
debug('[nuq+ `%s`] setState: %O', keys, newState)
144145
for (let [key, value] of Object.entries(newState)) {
145-
const config = keyMap[key]
146-
if (!config) {
146+
const parser = keyMap[key]
147+
if (!parser) {
147148
continue
148149
}
149150
if (
150-
(options.clearOnDefault || clearOnDefault) &&
151+
(callOptions.clearOnDefault ??
152+
parser.clearOnDefault ??
153+
clearOnDefault) &&
151154
value !== null &&
152-
config.defaultValue !== undefined &&
153-
(config.eq ?? ((a, b) => a === b))(value, config.defaultValue)
155+
parser.defaultValue !== undefined &&
156+
(parser.eq ?? ((a, b) => a === b))(value, parser.defaultValue)
154157
) {
155158
value = null
156159
}
157160
emitter.emit(key, value)
158-
enqueueQueryStringUpdate(key, value, config.serialize ?? String, {
159-
// Call-level options take precedence over hook declaration options.
160-
history: options.history ?? history,
161-
shallow: options.shallow ?? shallow,
162-
scroll: options.scroll ?? scroll,
163-
throttleMs: options.throttleMs ?? throttleMs,
164-
startTransition: options.startTransition ?? startTransition
161+
enqueueQueryStringUpdate(key, value, parser.serialize ?? String, {
162+
// Call-level options take precedence over individual parser options
163+
// which take precedence over global options
164+
history: callOptions.history ?? parser.history ?? history,
165+
shallow: callOptions.shallow ?? parser.shallow ?? shallow,
166+
scroll: callOptions.scroll ?? parser.scroll ?? scroll,
167+
throttleMs: callOptions.throttleMs ?? parser.throttleMs ?? throttleMs,
168+
startTransition:
169+
callOptions.startTransition ??
170+
parser.startTransition ??
171+
startTransition
165172
})
166173
}
167174
return scheduleFlushToURL(router)

0 commit comments

Comments
 (0)