Announcing nuqs version 2

nuqs 2

Opening up to other React frameworks

François Best

@fortysevenfx

Tuesday 22 October 2024


[email protected] is available, try it now:

pnpm add nuqs@latest

It’s packing exciting features & improvements, including:


Hello, React! 👋 ⚛️

nuqs started as a Next.js-only hook, and v2 brings compatibility for other React frameworks:

No code change is necessary in components that use nuqs hooks, making them universal across all supported frameworks.

The only new requirement is to wrap your React tree with an adapter for your framework.

Example for a React SPA with Vite:

src/main.tsx
import { NuqsAdapter } from 'nuqs/adapters/react'
 
createRoot(document.getElementById('root')!).render(
  <NuqsAdapter>
    <App />
  </NuqsAdapter>
)

The adapters documentation has examples for all supported frameworks.

Testing

One of the major pain points with nuqs v1 was testing components that used its hooks.

Nuqs v2 comes with a built-in testing adapter that mocks URL behaviours, allowing you to test your components in isolation, outside of any framework runtime.

You can use it with any unit testing framework that renders React components (I recommend Vitest & Testing Library).

counter-button.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 />, {
    // 1. Setup the test by passing initial search params / querystring:
    wrapper: ({ children }) => (
      <NuqsTestingAdapter searchParams="?count=42" onUrlUpdate={onUrlUpdate}>
        {children}
      </NuqsTestingAdapter>
    )
  })
  // 2. Act
  const button = screen.getByRole('button')
  await user.click(button)
  // 3. Assert changes in the state and in the (mocked) URL
  expect(button).toHaveTextContent('count is 43')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=43')
  expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('43')
  expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push')
})

The adapter conforms to the setup / act / assert testing strategy, allowing you to:

  1. Set the initial URL search params
  2. Let your test framework perform actions on your component
  3. Asserting on how the URL was changed as a result

Breaking changes & migration

The biggest breaking change is the introduction of adapters. Another one is related to deprecated APIs.

The next-usequerystate package that started this journey is no longer updated. All updates are now published under the nuqs package name.

The minimum version of Next.js supported is now 14.2.0. It is compatible with Next.js 15, including the async searchParams page prop in the server-side cache.

There are some important behaviour changes, based on feedback from the community:

Read the complete migration guide to update your applications.

Bundle size improvements

By moving to ESM-only, and dropping hacks needed to support older versions of Next.js, the bundle size is now 20% smaller than v1. It’s also side-effects free and tree-shakable.

What’s next?

The community and I have a lot of ideas for the future of nuqs, including:

Thanks

I want to thank sponsors, contributors and people who raised issues and discussions on GitHub and X/Twitter. You are the growing community that drives this project forward, and I couldn’t be happier with the response.

Sponsors

Thanks to these amazing people, I’m able to dedicate more time to this project and make it better for everyone. Join them on GitHub Sponsors!

Contributors

Huge thanks to @andreisocaciu, @tordans, @prasannamestha, @Talent30, @neefrehman, @chbg, @dopry, @weisisheng, @hugotiger, @iuriizaporozhets, @rikbrown, @mateogianolio, @timheerwagen, @psdmsft, and @psdewar for helping!