Announcing nuqs version 2
nuqs

Migration guide to v2

How to update your code to use [email protected]

Here’s a summary of the breaking changes in [email protected]:

Adapters

The biggest change is that [email protected] now supports other React frameworks, providing type-safe URL state for all.

You will need to wrap your app with the appropriate adapter for your framework or router, to let the hooks know how to interact with it.

Adapters are currently available for:

  • Next.js (app & pages routers)
  • React SPA
  • Remix
  • React Router
  • Testing environments (Vitest, Jest, etc.)

If you are coming from nuqs v1 (which only supported Next.js), you’ll need to wrap your app with the appropriate NuqsAdapter:

Next.js

Minimum required version: next@>=14.2.0

Early versions of Next.js 14 were in flux with regards to shallow routing. Supporting those earlier versions required a lot of hacks, workarounds, and performance penalties, which were removed in [email protected].

App router

src/app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { type ReactNode } from 'react'
 
export default function RootLayout({
  children
}: {
  children: ReactNode
}) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  )
}

Pages router

src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
 
export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <NuqsAdapter>
      <Component {...pageProps} />
    </NuqsAdapter>
  )
}

Unified (router-agnostic)

If your Next.js app uses both the app and pages routers and the adapter needs to be mounted in either, you can import the unified adapter, at the cost of a slightly larger bundle size (~100B).

import { NuqsAdapter } from 'nuqs/adapters/next'

Other adapters

Albeit not part of a migration from v1, you can now use nuqs in other React frameworks via their respective adapters.

However, there’s one more adapter that might be of interest to you, and solves a long-standing issue with testing components using nuqs hooks:

Testing adapter

Unit-testing components that used nuqs v1 was a hassle, as it required mocking the Next.js router internals, causing abstraction leaks.

In v2, you can now wrap your components to test with the NuqsTestingAdapter, which provides a convenient setup & assertion API for your tests.

Here’s an example with Vitest & Testing Library:

counter-button-example.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'
 
it('should increment the count when clicked', async () => {
  const user = userEvent.setup()
  const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
  render(<CounterButton />, {
    // Setup the test by passing initial search params / querystring:
    wrapper: ({ children }) => (
      <NuqsTestingAdapter searchParams="?count=1" onUrlUpdate={onUrlUpdate}>
        {children}
      </NuqsTestingAdapter>
    )
  })
  // Act
  const button = screen.getByRole('button')
  await user.click(button)
  // Assert changes in the state and in the (mocked) URL
  expect(button).toHaveTextContent('count is 2')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=2')
  expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('2')
  expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push')
})

Behaviour changes

Setting the startTransition option no longer sets shallow: false automatically. This is to align with other frameworks that don’t have a concept of shallow/deep routing.

You’ll have to set both to keep sending updates to the server and getting a loading state in Next.js:

useQueryState('q', {
  startTransition: true,
+ shallow: false
})

The "use client" directive was not included in the client import (import {} from 'nuqs'). It has now been added, meaning that server-side code needs to import from nuqs/server to avoid errors like:

Error: Attempted to call withDefault() from the server but withDefault is on
the client. It's not possible to invoke a client function from the server, it can
only be rendered as a Component or passed to props of a Client
Component.

ESM only

[email protected] is now an ESM-only package. This should not be much of an issue since Next.js supports ESM in app code since version 12, but if you are bundling nuqs code into an intermediate CJS library to be consumed in Next.js, you’ll run into import issues:

[ERR_REQUIRE_ESM]: require() of ES Module not supported

If converting your library to ESM is not possible, your main option is to dynamically import nuqs:

const { useQueryState } = await import('nuqs')

Deprecated exports

Some of the v1 API was marked as deprecated back in September 2023, and has been removed in [email protected].

queryTypes parsers object

The queryTypes object has been removed in favor of individual parser exports, for better tree-shaking.

Replace with parseAsXYZ to match:

- import { queryTypes } from 'nuqs'
+ import { parseAsString, parseAsInteger, ... } from 'nuqs'
 
- useQueryState('q',    queryTypes.string.withOptions({ ... }))
- useQueryState('page', queryTypes.integer.withDefault(1))
+ useQueryState('q',    parseAsString.withOptions({ ... }))
+ useQueryState('page', parseAsInteger.withDefault(1))

subscribeToQueryUpdates

Next.js 14.1.0 makes useSearchParams reactive to shallow search params updates, which makes this internal helper function redundant. See #425 for context.

Renamed nuqs/parsers to nuqs/server

When introducing the server cache in #397, the dedicated export for parsers was reused as it didn’t include the "use client" directive. Since it now contains more than parsers and probably will be extended with server-only code in the future, it has been renamed to a clearer export name.

Find and replace all occurrences of nuqs/parsers to nuqs/server in your code:

- import { parseAsInteger, createSearchParamsCache } from 'nuqs/parsers'
+ import { parseAsInteger, createSearchParamsCache } from 'nuqs/server'

Debug printout detection

After the rename to nuqs, the debugging printout detection logic handled either next-usequerystate or nuqs being present in the localStorage.debug variable. [email protected] only checks for the presence of the nuqs substring to enable logs.

Update your local dev environments to match by running this once in the devtools console:

if (localStorage.debug) {
  localStorage.debug = localStorage.debug.replace('next-usequerystate', 'nuqs')
}

Dropping next-usequerystate

This package started under the name next-usequerystate, and was renamed to nuqs in January 2024. The old package name was kept as an alias for the v1 release line.

nuqs version 2 and onwards no longer mirror to the next-usequerystate package name.

Type changes

The following breaking changes only apply to exported types:

  • The Options type is no longer generic
  • The UseQueryStatesOptions is now a type rather than an interface, and is now generic over the type of the object you pass to useQueryStates.
  • parseAsJson now requires a runtime validation function to infer the type of the parsed JSON data.

On this page