Announcing nuqs version 2
nuqs

Options

Configuring nuqs

By default, nuqs will update search params:

  1. On the client only (not sending requests to the server),
  2. by replacing the current history entry,
  3. and without scrolling to the top of the page.

These behaviours can be configured, along with a few additional options.

Passing options

Options can be passed at the hook level via the builder pattern:

const [state, setState] = useQueryState(
  'foo',
  parseAsString.withOptions({ history: 'push' })
)

Or when calling the state updater function, as a second parameter:

setState('foo', { scroll: true })

Call-level options will override hook level options.

History

By default, state updates are done by replacing the current history entry with the updated query when state changes.

You can see this as a sort of git squash, where all state-changing operations are merged into a single browsing history entry.

You can also opt-in to push a new history entry for each state change, per key, which will let you use the Back button to navigate state updates:

// Append state changes to history:
useQueryState('foo', { history: 'push' })

Watch out!

Breaking the Back button can lead to a bad user experience. Make sure to use this option only if the search params to update contribute to a navigation-like experience (eg: tabs, modals). Overriding the Back behaviour must be a UX enhancement rather than a nuisance.

— “With great power comes great responsibility.”

Shallow

By default, query state updates are done in a client-first manner: there are no network calls to the server.

This is equivalent to the shallow option of the Next.js router set to true.

To opt-in to notifying the server on query updates, you can set shallow to false:

useQueryState('foo', { shallow: false })

Note that the shallow option only makes sense if your page can be server-side rendered. Therefore, it’s a no-op in React SPA.

For server-side renderable frameworks, you would pair shallow: false with:

  • In Next.js app router: the searchParams page prop to render the RSC tree based on the updated query state.
  • In Next.js pages router: the getServerSideProps function
  • In Remix & React Router: a loader function

In React Router based frameworks

While the shallow: false default behaviour is uncommon for Remix and React Router, where loaders are always supposed to run on URL changes, nuqs gives you control of this behaviour, by opting in to running loaders only if they do need to access the relevant search params.

One caveat is that the stock useSearchParams hook from those frameworks doesn’t reflect shallow-updated search params, so we provide you with one that does:

import { useOptimisticSearchParams } from 'nuqs/adapters/remix' // or '…/react-router/v6' or '…/react-router/v7'
 
function Component() {
  // Note: this is read-only, but reactive to all URL changes
  const searchParams = useOptimisticSearchParams()
  return <div>{searchParams.get('foo')}</div>
}

This concept of “shallow routing” is done via updates to the browser’s History API.

While the useOptimisticSearchParams and the adapter itself can handle shallow URL updates triggered from state updater functions, for them to react to URL changes triggered by explicit calls to the History API (either by first or third party code), you’d have to enable sync:

// Export available in:
//  'nuqs/adapters/remix'
//  'nuqs/adapters/react-router/v6'
//  'nuqs/adapters/react-router/v7'
//  'nuqs/adapters/react'
import { enableHistorySync } from 'nuqs/adapters/remix'
 
// Somewhere top-level (like app/root.tsx)
enableHistorySync()

Note that you may not need this if only using your framework’s router.

It is opt-in as it patches the History APIs, which can have side effects if third party code does it too.

Scroll

The Next.js router scrolls to the top of the page on navigation updates, which may not be desirable when updating the query string with local state.

Query state updates won’t scroll to the top of the page by default, but you can opt-in to this behaviour:

useQueryState('foo', { scroll: true })

Throttling URL updates

Because of browsers rate-limiting the History API, updates to the URL are queued and throttled to a default of 50ms, which seems to satisfy most browsers even when sending high-frequency query updates, like binding to a text input or a slider.

Safari’s rate limits are much higher and require a throttle of 120ms (320ms for older versions of Safari).

If you want to opt-in to a larger throttle time — for example to reduce the amount of requests sent to the server when paired with shallow: false — you can specify it under the throttleMs option:

useQueryState('foo', {
  // Send updates to the server maximum once every second
  shallow: false,
  throttleMs: 1000
})

Note

the state returned by the hook is always updated instantly, to keep UI responsive. Only changes to the URL, and server requests when using shallow: false, are throttled.

If multiple hooks set different throttle values on the same event loop tick, the highest value will be used. Also, values lower than 50ms will be ignored, to avoid rate-limiting issues. Read more.

Specifying a +Infinity value for throttleMs will disable updates to the URL or the server, but all useQueryState(s) hooks will still update their internal state and stay in sync with each other.

Transitions

When combined with shallow: false, you can use React’s useTransition hook to get loading states while the server is re-rendering server components with the updated URL.

Pass in the startTransition function from useTransition to the options to enable this behaviour:

Upcoming changes

In nuqs@2.0.0, passing startTransition will no longer automatically set shallow: false.

'use client'
 
import React from 'react'
import { useQueryState, parseAsString } from 'nuqs'
 
function ClientComponent({ data }) {
  // 1. Provide your own useTransition hook:

  const [isLoading, startTransition] = React.useTransition()
  const [query, setQuery] = useQueryState(
    'query',
    // 2. Pass the `startTransition` as an option:

    parseAsString().withOptions({ startTransition, shallow: false })
  )
  // 3. `isLoading` will be true while the server is re-rendering
  // and streaming RSC payloads, when the query is updated via `setQuery`.
 
  // Indicate loading state
  if (isLoading) return <div>Loading...</div>
 
  // Normal rendering with data
  return <div>...</div>
}

In nuqs@1.x.x, passing startTransition as an option automatically sets shallow: false.

This is no longer the case in nuqs@>=2.0.0: you’ll need to set it explicitly.

Clear on default

When the state is set to the default value, the search parameter is removed from the URL, instead of being reflected explicitly.

However, sometimes you might want to keep the search parameter in the URL, because default values can change, and the meaning of the URL along with it.

Example of defaults changing

In nuqs@1.x.x, clearOnDefault was false by default.
in nuqs@2.0.0, clearOnDefault is now true by default, in response to user feedback.

If you want to keep the search parameter in the URL when it’s set to the default value, you can set clearOnDefault to false:

useQueryState('search', {
  defaultValue: '',
  clearOnDefault: false
})

Tip

Clearing the key-value pair from the query string can always be done by setting the state to null.

This option compares the set state against the default value using === reference equality, so if you are using a custom parser for a state type that wouldn’t work with reference equality, you should provide the eq function to your parser (this is done for you in built-in parsers):

const dateParser = createParser({
  parse: (value: string) => new Date(value.slice(0, 10)),
  serialize: (date: Date) => date.toISOString().slice(0, 10),
  eq: (a: Date, b: Date) => a.getTime() === b.getTime() 
})

On this page