Skip to content

Commit

Permalink
fix: Allow parser-level options in useQueryStates (#621)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
franky47 authored Sep 1, 2024
1 parent e2c88a6 commit 099ceb3
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 19 deletions.
20 changes: 19 additions & 1 deletion packages/docs/content/docs/batching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: useQueryStates
description: How to read & update multiple search params at once
---

## Multiple Queries (batching)
## Multiple updates (batching)

You can call as many state update function as needed in a single event loop
tick, and they will be applied to the URL asynchronously:
Expand Down Expand Up @@ -80,3 +80,21 @@ const search = await setCoordinates({
lng: Math.random() * 360 - 180
})
```

### Options

There are three places you can define [options](./options) in `useQueryStates`:
- As the second argument to the hook itself (global options, see above)
- On each parser, like `parseAsFloat.withOptions({ shallow: false }){:ts}`
- At the call level when updating the state:

```ts
setCoordinates({
lat: 42,
lng: 12
}, {
shallow: false
})
```

The order of precedence is: call-level options > parser options > global options.
16 changes: 16 additions & 0 deletions packages/e2e/cypress/e2e/useQueryStates-options.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// <reference types="cypress" />

it('useQueryStates options', () => {
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#1').click()
cy.location('search').should('eq', '?b=')
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#2').click()
cy.location('search').should('eq', '?a=&b=')
cy.visit('/app/useQueryStates-options?a=foo&b=bar')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#3').click()
cy.location('search').should('eq', '')
})
45 changes: 45 additions & 0 deletions packages/e2e/src/app/app/useQueryStates-options/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import { parseAsString, useQueryStates } from 'nuqs'
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense>
<Client />
</Suspense>
)
}

function Client() {
const [values, setValues] = useQueryStates(
{
a: parseAsString.withDefault(''),
b: parseAsString.withDefault('').withOptions({
clearOnDefault: false
})
},
{
clearOnDefault: true
}
)
return (
<>
<button id="1" onClick={() => setValues({ a: '', b: '' })}>
1
</button>
<button
id="2"
onClick={() => setValues({ a: '', b: '' }, { clearOnDefault: false })}
>
2
</button>
<button
id="3"
onClick={() => setValues({ a: '', b: '' }, { clearOnDefault: true })}
>
3
</button>
</>
)
}
5 changes: 4 additions & 1 deletion packages/nuqs/src/update-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export function enqueueQueryStringUpdate<Value>(
key: string,
value: Value | null,
serialize: (value: Value) => string,
options: Options
options: Pick<
Options,
'history' | 'scroll' | 'shallow' | 'startTransition' | 'throttleMs'
>
) {
const serializedOrNull = value === null ? null : serialize(value)
debug('[nuqs queue] Enqueueing %s=%s %O', key, serializedOrNull, options)
Expand Down
2 changes: 1 addition & 1 deletion packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export function useQueryState<T = string>(
? stateUpdater(stateRef.current ?? defaultValue ?? null)
: stateUpdater
if (
(options.clearOnDefault || clearOnDefault) &&
(options.clearOnDefault ?? clearOnDefault) &&
newValue !== null &&
defaultValue !== undefined &&
eq(newValue, defaultValue)
Expand Down
39 changes: 23 additions & 16 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {
} from './update-queue'
import { safeParse } from './utils'

type KeyMapValue<Type> = Parser<Type> & {
defaultValue?: Type
}
type KeyMapValue<Type> = Parser<Type> &
Options & {
defaultValue?: Type
}

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

const update = React.useCallback<SetValues<KeyMap>>(
(stateUpdater, options = {}) => {
(stateUpdater, callOptions = {}) => {
const newState: Partial<Nullable<KeyMap>> =
typeof stateUpdater === 'function'
? stateUpdater(stateRef.current)
: stateUpdater
debug('[nuq+ `%s`] setState: %O', keys, newState)
for (let [key, value] of Object.entries(newState)) {
const config = keyMap[key]
if (!config) {
const parser = keyMap[key]
if (!parser) {
continue
}
if (
(options.clearOnDefault || clearOnDefault) &&
(callOptions.clearOnDefault ??
parser.clearOnDefault ??
clearOnDefault) &&
value !== null &&
config.defaultValue !== undefined &&
(config.eq ?? ((a, b) => a === b))(value, config.defaultValue)
parser.defaultValue !== undefined &&
(parser.eq ?? ((a, b) => a === b))(value, parser.defaultValue)
) {
value = null
}
emitter.emit(key, value)
enqueueQueryStringUpdate(key, value, config.serialize ?? String, {
// Call-level options take precedence over hook declaration options.
history: options.history ?? history,
shallow: options.shallow ?? shallow,
scroll: options.scroll ?? scroll,
throttleMs: options.throttleMs ?? throttleMs,
startTransition: options.startTransition ?? startTransition
enqueueQueryStringUpdate(key, value, parser.serialize ?? String, {
// Call-level options take precedence over individual parser options
// which take precedence over global options
history: callOptions.history ?? parser.history ?? history,
shallow: callOptions.shallow ?? parser.shallow ?? shallow,
scroll: callOptions.scroll ?? parser.scroll ?? scroll,
throttleMs: callOptions.throttleMs ?? parser.throttleMs ?? throttleMs,
startTransition:
callOptions.startTransition ??
parser.startTransition ??
startTransition
})
}
return scheduleFlushToURL(router)
Expand Down

0 comments on commit 099ceb3

Please sign in to comment.