Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions errors/NUQS-422.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Invalid Options Combination.

This warning will show up if you combine `shallow: true` (the default) and `limitUrlUpdates: debounce` options.
This warning did show up between versions `nuqs@>=2.6.0 && nuqs@<=2.7.2` if you combined `shallow: true` (the default) and `limitUrlUpdates: debounce` options.

Debounce only makes sense for server-side data fetching, the returned client state is always updated **immediately**, so combining `limitUrlUpdates: debounce` with `shallow: true` will not work as expected.
The initial argument was that:

If you are fetching client-side, you’ll want to debounce the state returned by the hooks instead (using a 3rd party `useDebounce` utility hook).
> Debounce only made sense for server-side data fetching, the returned client state is always updated **immediately**, so combining `limitUrlUpdates: debounce` with `shallow: true` will not work as expected.
>
> If you are fetching client-side, you’ll want to debounce the state returned by the hooks instead (using a 3rd party `useDebounce` utility hook).

## Solution
However, debounce _does_ have a purpose even in shallow routing: **reducing history bloat**.

- Set `shallow: false` to allow debounce to work properly, check the [documentation](https://nuqs.dev/docs/options#debounce) for more information.
While nuqs uses `history: replace` by default to avoid polluting your history **stack** (the one you navigate with the Back & Forward browser buttons), every URL update is recorded in the browser's **global history** (that you access via a menu, like `⌘ Y`).

Debouncing gives you finer control over how this global history is populated, with the trade-off of a less reactive URL.

Regardless, debounce only applies to URL updates, so the recommendation for client-side fetching still stands: you'll likely want to debounce the returned state in userland before feeding it to TanStack Query, SWC, tRPC or other tools.
1 change: 0 additions & 1 deletion packages/nuqs/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export const errors = {
404: 'nuqs requires an adapter to work with your framework.',
409: 'Multiple versions of the library are loaded. This may lead to unexpected behavior. Currently using `%s`, but `%s` (via the %s adapter) was about to load on top.',
414: 'Max safe URL length exceeded. Some browsers may not be able to accept this URL. Consider limiting the amount of state stored in the URL.',
422: 'Invalid options combination: `limitUrlUpdates: debounce` should be used in SSR scenarios, with `shallow: false`',
429: 'URL update rate-limited by the browser. Consider increasing `throttleMs` for key(s) `%s`. %O',
500: "Empty search params cache. Search params can't be accessed in Layouts.",
501: 'Search params cache already populated. Have you called `parse` twice?'
Expand Down
21 changes: 8 additions & 13 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import type { Nullable, Options, UrlKeys } from './defs'
import { compareQuery } from './lib/compare'
import { debug } from './lib/debug'
import { error } from './lib/errors'
import { debounceController } from './lib/queues/debounce'
import { defaultRateLimit } from './lib/queues/rate-limiting'
import {
Expand Down Expand Up @@ -114,10 +113,9 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
const queuedQueries = debounceController.useQueuedQueries(
Object.values(resolvedUrlKeys)
)
const [internalState, setInternalState] = useState<V>(() => {
const source = initialSearchParams ?? new URLSearchParams()
return parseMap(keyMap, urlKeys, source, queuedQueries).state
})
const [internalState, setInternalState] = useState<V>(
() => parseMap(keyMap, urlKeys, initialSearchParams, queuedQueries).state
)

const stateRef = useRef(internalState)
debug(
Expand Down Expand Up @@ -158,8 +156,8 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
return [
urlKey,
parser?.type === 'multi'
? initialSearchParams?.getAll(urlKey)
: (initialSearchParams?.get(urlKey) ?? null)
? initialSearchParams.getAll(urlKey)
: (initialSearchParams.get(urlKey) ?? null)
]
})
)
Expand Down Expand Up @@ -187,7 +185,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
}
}, [
Object.values(resolvedUrlKeys)
.map(key => `${key}=${initialSearchParams?.getAll(key)}`)
.map(key => `${key}=${initialSearchParams.getAll(key)}`)
.join('&'),
JSON.stringify(queuedQueries)
])
Expand Down Expand Up @@ -324,9 +322,6 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
limitUrlUpdates?.method === 'debounce' ||
parser.limitUrlUpdates?.method === 'debounce'
) {
if (update.options.shallow === true) {
console.warn(error(422))
}
const timeMs =
callOptions?.limitUrlUpdates?.timeMs ??
limitUrlUpdates?.timeMs ??
Expand Down Expand Up @@ -412,8 +407,8 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
const query =
queuedQuery === undefined
? ((parser.type === 'multi'
? searchParams?.getAll(urlKey)
: searchParams?.get(urlKey)) ?? fallbackValue)
? searchParams.getAll(urlKey)
: searchParams.get(urlKey)) ?? fallbackValue)
: queuedQuery
if (
cachedQuery &&
Expand Down
Loading