Convert Figma logo to code with AI

47ng logonuqs

Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.

5,289
119
5,289
29

Top Related Projects

42,139

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.

30,395

React Hooks for Data Fetching

8,432

🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering.

Declarative routing for React

34,688

🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.

The official, opinionated, batteries-included toolset for efficient Redux development

Quick Overview

The 47ng/nuqs repository is a TypeScript-based framework for building modern, reactive user interfaces. It provides a lightweight and efficient alternative to larger frameworks like React or Angular, focusing on simplicity and developer productivity.

Pros

  • Lightweight and Fast: Nuqs is designed to be lightweight and performant, with a small footprint and fast rendering.
  • Reactive Approach: Nuqs follows a reactive programming model, making it easy to build responsive and dynamic user interfaces.
  • TypeScript Support: Nuqs is written in TypeScript, providing type safety and better tooling support.
  • Simple and Intuitive API: The Nuqs API is straightforward and easy to learn, with a focus on developer productivity.

Cons

  • Smaller Community: Compared to larger frameworks, Nuqs has a smaller community and ecosystem, which may limit the availability of third-party libraries and resources.
  • Limited Documentation: The project's documentation, while improving, could be more comprehensive and detailed.
  • Newer Framework: As a relatively new framework, Nuqs may not have the same level of maturity and stability as more established options.
  • Limited Tooling: The tooling and ecosystem around Nuqs may not be as robust as those for larger frameworks.

Code Examples

Here are a few code examples to give you a sense of how Nuqs works:

Creating a Simple Component

import { component, html } from 'nuqs';

const MyComponent = component({
  render() {
    return html`
      <div>
        <h1>Hello, Nuqs!</h1>
        <p>This is a simple Nuqs component.</p>
      </div>
    `;
  },
});

Handling User Interactions

import { component, html, useState } from 'nuqs';

const CounterComponent = component({
  render() {
    const [count, setCount] = useState(0);

    return html`
      <div>
        <h1>Counter</h1>
        <p>The current count is: ${count}</p>
        <button @click=${() => setCount(count + 1)}>Increment</button>
      </div>
    `;
  },
});

Using Reactive State

import { component, html, reactive } from 'nuqs';

const TodoComponent = component({
  render() {
    const state = reactive({
      todos: ['Learn Nuqs', 'Build a Todo App', 'Profit'],
      newTodo: '',
    });

    const addTodo = () => {
      state.todos.push(state.newTodo);
      state.newTodo = '';
    };

    return html`
      <div>
        <h1>Todo List</h1>
        <ul>
          ${state.todos.map((todo) => html`<li>${todo}</li>`)}
        </ul>
        <input
          type="text"
          .value=${state.newTodo}
          @input=${(e) => (state.newTodo = (e.target as HTMLInputElement).value)}
        />
        <button @click=${addTodo}>Add Todo</button>
      </div>
    `;
  },
});

Getting Started

To get started with Nuqs, you can follow these steps:

  1. Install Nuqs using npm or yarn:

    npm install nuqs
    # or
    yarn add nuqs
    
  2. Create a new Nuqs component:

    import { component, html } from 'nuqs';
    
    const MyComponent = component({
      render() {
        return html`
          <div>
            <h1>Hello, Nuqs!</h1>
            <p>This is a simple Nuqs component.</p>
          </div>
        `;
      },
    });
    
  3. Mount the component to the DOM:

    import { render } from 'nuqs';
    
    render(MyComponent, document.getElementById('app'));
    
  4. Start your development server and open your application in a web browser.

For more detailed information, please

Competitor Comparisons

42,139

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.

Pros of TanStack Query

  • More comprehensive data fetching and state management solution
  • Supports multiple frameworks (React, Vue, Svelte, etc.)
  • Larger community and ecosystem with extensive documentation

Cons of TanStack Query

  • Steeper learning curve due to its broader feature set
  • Potentially overkill for simple use cases
  • Larger bundle size compared to nuqs

Code Comparison

nuqs:

import { useQueryState } from 'nuqs'

const [value, setValue] = useQueryState('key')

TanStack Query:

import { useQuery } from '@tanstack/react-query'

const { data, isLoading } = useQuery('key', fetchData)

Summary

nuqs is a lightweight solution focused on URL query state management, while TanStack Query offers a more comprehensive data fetching and caching solution. nuqs is simpler to use for basic URL query manipulation, whereas TanStack Query provides more advanced features for complex data management scenarios across multiple frameworks. The choice between the two depends on the specific requirements of your project and the level of complexity you need to handle.

30,395

React Hooks for Data Fetching

Pros of SWR

  • More comprehensive data fetching and caching solution
  • Supports real-time and optimistic updates
  • Larger community and ecosystem

Cons of SWR

  • Steeper learning curve for beginners
  • Potentially overkill for simple use cases
  • Requires more setup and configuration

Code Comparison

SWR:

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)
  if (error) return <div>Failed to load</div>
  if (!data) return <div>Loading...</div>
  return <div>Hello {data.name}!</div>
}

nuqs:

import { useQueryState } from 'nuqs'

function SearchComponent() {
  const [query, setQuery] = useQueryState('q')
  return (
    <input
      value={query ?? ''}
      onChange={e => setQuery(e.target.value)}
    />
  )
}

Summary

SWR is a more feature-rich data fetching library, while nuqs focuses specifically on managing URL query parameters. SWR offers advanced caching and real-time updates, making it suitable for complex applications. nuqs, on the other hand, provides a simpler solution for handling URL query states, which can be beneficial for projects that primarily need URL parameter management without the overhead of a full data fetching library.

8,432

🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering.

Pros of TanStack Router

  • More comprehensive routing solution with advanced features like nested routes and route guards
  • Better suited for large, complex applications with intricate navigation requirements
  • Offers type-safe routing with TypeScript integration

Cons of TanStack Router

  • Steeper learning curve due to its more complex API and concepts
  • Potentially overkill for simple applications or those primarily focused on URL query string management

Code Comparison

nuqs:

import { useQueryState } from 'nuqs'

const [value, setValue] = useQueryState('key')

TanStack Router:

import { Route, Router } from '@tanstack/react-router'

const rootRoute = new Route({
  path: '/',
  component: RootComponent,
})

const router = new Router({ routes: [rootRoute] })

Summary

nuqs is a lightweight library focused on managing URL query parameters, making it ideal for simpler use cases. TanStack Router, on the other hand, is a full-featured routing solution that offers more advanced capabilities but comes with increased complexity. The choice between the two depends on the specific needs of your project, with nuqs being more suitable for straightforward query string management and TanStack Router for comprehensive routing requirements in larger applications.

Declarative routing for React

Pros of React Router

  • Comprehensive routing solution with nested routes, dynamic segments, and more
  • Large, active community and extensive documentation
  • Seamless integration with React ecosystem and server-side rendering

Cons of React Router

  • Larger bundle size and potential performance overhead
  • Steeper learning curve for complex routing scenarios
  • Less focused on URL state management compared to nuqs

Code Comparison

React Router:

import { BrowserRouter, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Route path="/users/:id" component={UserProfile} />
    </BrowserRouter>
  );
}

nuqs:

import { useQueryState } from "nuqs";

function App() {
  const [userId, setUserId] = useQueryState("userId");
  return <UserProfile id={userId} />;
}

Summary

React Router is a full-featured routing library for React applications, offering a wide range of routing capabilities. It's well-suited for complex applications with nested routes and dynamic navigation. On the other hand, nuqs focuses specifically on URL query string management, providing a simpler and more lightweight solution for handling URL state. While React Router excels in overall routing, nuqs offers a more targeted approach to query parameter management with potentially less overhead.

34,688

🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.

Pros of tRPC

  • Full-stack type safety for APIs, reducing runtime errors
  • Automatic API documentation generation
  • Supports real-time subscriptions and file uploads

Cons of tRPC

  • Requires TypeScript and may not be suitable for non-TS projects
  • Steeper learning curve compared to simpler URL parameter libraries

Code Comparison

tRPC:

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const appRouter = t.router({
  hello: t.procedure.query(() => 'Hello, World!'),
});

nuqs:

import { useQueryState } from 'nuqs';

const [name, setName] = useQueryState('name');
console.log(name); // Value from URL parameter

Summary

tRPC offers a comprehensive solution for end-to-end type-safe APIs, while nuqs focuses on managing URL parameters in React applications. tRPC provides more features but requires a deeper integration into the project structure. nuqs is simpler to use for its specific use case of handling URL parameters in React apps.

The official, opinionated, batteries-included toolset for efficient Redux development

Pros of Redux Toolkit

  • More comprehensive state management solution with built-in tools for immutability and async operations
  • Widely adopted in the React ecosystem with extensive documentation and community support
  • Includes utilities for creating and managing slices of state, simplifying reducer logic

Cons of Redux Toolkit

  • Steeper learning curve due to its more complex architecture and concepts
  • Can be overkill for simpler applications or those with minimal state management needs
  • Requires more boilerplate code compared to simpler solutions

Code Comparison

Redux Toolkit:

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
  },
})

nuqs:

import { useQueryState } from 'nuqs'

const [count, setCount] = useQueryState('count', {
  defaultValue: '0',
  parse: Number,
})

Key Differences

  • Redux Toolkit focuses on global state management, while nuqs specializes in URL query string state
  • nuqs offers a simpler API for managing URL-based state, making it easier to implement and use
  • Redux Toolkit provides more advanced features for complex state management scenarios

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

nuqs

NPM MIT License Continuous Integration Depfu

Type-safe search params state manager for React frameworks. Like useState, but stored in the URL query string.

Features

  • 🔀 new: Supports Next.js (app and pages routers), plain React (SPA), Remix, React Router, and custom routers via adapters
  • 🧘‍♀️ Simple: the URL is the source of truth
  • 🕰 Replace history or append to use the Back button to navigate state updates
  • ⚡️ Built-in parsers for common state types (integer, float, boolean, Date, and more). Create your own parsers for custom types & pretty URLs
  • ♊️ Related querystrings with useQueryStates
  • 📡 Shallow mode by default for URL query updates, opt-in to notify server components
  • 🗃 Server cache for type-safe searchParams access in nested server components
  • ⌛️ Support for useTransition to get loading states on server updates

Documentation

Read the complete documentation at nuqs.47ng.com.

Installation

pnpm add nuqs
yarn add nuqs
npm install nuqs

Adapters

You will need to wrap your React component tree with an adapter for your framework. (expand the appropriate section below)

▲ Next.js (app router)

Supported Next.js versions: >=14.2.0. For older versions, install nuqs@^1 (which doesn't need this adapter code).

// 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>
  )
}
▲ Next.js (pages router)

Supported Next.js versions: >=14.2.0. For older versions, install nuqs@^1 (which doesn't need this adapter code).

// 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>
  )
}
⚛️ Plain React (SPA)

Example: via Vite or create-react-app.

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

createRoot(document.getElementById('root')!).render(
  <NuqsAdapter>
    <App />
  </NuqsAdapter>
)
💿 Remix

Supported Remix versions: @remix-run/react@>=2

// app/root.tsx
import { NuqsAdapter } from 'nuqs/adapters/remix'

// ...

export default function App() {
  return (
    <NuqsAdapter>
      <Outlet />
    </NuqsAdapter>
  )
}
React Router v6

Supported React Router versions: react-router-dom@^6

import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import App from './App'

const router = createBrowserRouter([
  {
    path: '/',
    element: <App />
  }
])

export function ReactRouter() {
  return (
    <NuqsAdapter>
      <RouterProvider router={router} />
    </NuqsAdapter>
  )
}
React Router v7

Supported React Router versions: react-router@^7

// app/root.tsx
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'
import { Outlet } from 'react-router'

// ...

export default function App() {
  return (
    <NuqsAdapter>
      <Outlet />
    </NuqsAdapter>
  )
}

Usage

'use client' // Only works in client components

import { useQueryState } from 'nuqs'

export default () => {
  const [name, setName] = useQueryState('name')
  return (
    <>
      <h1>Hello, {name || 'anonymous visitor'}!</h1>
      <input value={name || ''} onChange={e => setName(e.target.value)} />
      <button onClick={() => setName(null)}>Clear</button>
    </>
  )
}

useQueryState takes one required argument: the key to use in the query string.

Like React.useState, it returns an array with the value present in the query string as a string (or null if none was found), and a state updater function.

Example outputs for our hello world example:

URLname valueNotes
/nullNo name key in URL
/?name=''Empty string
/?name=foo'foo'
/?name=2'2'Always returns a string by default, see Parsing below

Parsing

If your state type is not a string, you must pass a parsing function in the second argument object.

We provide parsers for common and more advanced object types:

import {
  parseAsString,
  parseAsInteger,
  parseAsFloat,
  parseAsBoolean,
  parseAsTimestamp,
  parseAsIsoDateTime,
  parseAsArrayOf,
  parseAsJson,
  parseAsStringEnum,
  parseAsStringLiteral,
  parseAsNumberLiteral
} from 'nuqs'

useQueryState('tag') // defaults to string
useQueryState('count', parseAsInteger)
useQueryState('brightness', parseAsFloat)
useQueryState('darkMode', parseAsBoolean)
useQueryState('after', parseAsTimestamp) // state is a Date
useQueryState('date', parseAsIsoDateTime) // state is a Date
useQueryState('array', parseAsArrayOf(parseAsInteger)) // state is number[]
useQueryState('json', parseAsJson<Point>()) // state is a Point

// Enums (string-based only)
enum Direction {
  up = 'UP',
  down = 'DOWN',
  left = 'LEFT',
  right = 'RIGHT'
}

const [direction, setDirection] = useQueryState(
  'direction',
  parseAsStringEnum<Direction>(Object.values(Direction)) // pass a list of allowed values
    .withDefault(Direction.up)
)

// Literals (string-based only)
const colors = ['red', 'green', 'blue'] as const

const [color, setColor] = useQueryState(
  'color',
  parseAsStringLiteral(colors) // pass a readonly list of allowed values
    .withDefault('red')
)

// Literals (number-based only)
const diceSides = [1, 2, 3, 4, 5, 6] as const

const [side, setSide] = useQueryState(
  'side',
  parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values
    .withDefault(4)
)

You may pass a custom set of parse and serialize functions:

import { useQueryState } from 'nuqs'

export default () => {
  const [hex, setHex] = useQueryState('hex', {
    // TypeScript will automatically infer it's a number
    // based on what `parse` returns.
    parse: (query: string) => parseInt(query, 16),
    serialize: value => value.toString(16)
  })
}

Using parsers in Server Components

Note: see the Accessing searchParams in server components section for a more user-friendly way to achieve type-safety.

If you wish to parse the searchParams in server components, you'll need to import the parsers from nuqs/server, which doesn't include the "use client" directive.

You can then use the parseServerSide method:

import { parseAsInteger } from 'nuqs/server'

type PageProps = {
  searchParams: {
    counter?: string | string[]
  }
}

const counterParser = parseAsInteger.withDefault(1)

export default function ServerPage({ searchParams }: PageProps) {
  const counter = counterParser.parseServerSide(searchParams.counter)
  console.log('Server side counter: %d', counter)
  return (
    ...
  )
}

See the server-side parsing demo for a live example showing how to reuse parser configurations between client and server code.

Note: parsers don't validate your data. If you expect positive integers or JSON-encoded objects of a particular shape, you'll need to feed the result of the parser to a schema validation library, like Zod.

Default value

When the query string is not present in the URL, the default behaviour is to return null as state.

It can make state updating and UI rendering tedious. Take this example of a simple counter stored in the URL:

import { useQueryState, parseAsInteger } from 'nuqs'

export default () => {
  const [count, setCount] = useQueryState('count', parseAsInteger)
  return (
    <>
      <pre>count: {count}</pre>
      <button onClick={() => setCount(0)}>Reset</button>
      {/* handling null values in setCount is annoying: */}
      <button onClick={() => setCount(c => c ?? 0 + 1)}>+</button>
      <button onClick={() => setCount(c => c ?? 0 - 1)}>-</button>
      <button onClick={() => setCount(null)}>Clear</button>
    </>
  )
}

You can specify a default value to be returned in this case:

const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0))

const increment = () => setCount(c => c + 1) // c will never be null
const decrement = () => setCount(c => c - 1) // c will never be null
const clearCount = () => setCount(null) // Remove query from the URL

Note: the default value is internal to React, it will not be written to the URL.

Setting the state to null will remove the key in the query string and set the state to the default value.

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 history value.

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

// Default: replace current history with new state
useQueryState('foo', { history: 'replace' })

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

Any other value for the history option will fallback to the default.

You can also override the history mode when calling the state updater function:

const [query, setQuery] = useQueryState('q', { history: 'push' })

// This overrides the hook declaration setting:
setQuery(null, { history: 'replace' })

Shallow

Note: this feature only applies to Next.js

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 pages router set to true, or going through the experimental windowHistorySupport flag in the app router.

To opt-in to query updates notifying the server (to re-run getServerSideProps in the pages router and re-render Server Components on the app router), you can set shallow to false:

const [state, setState] = useQueryState('foo', { shallow: false })

// You can also pass the option on calls to setState:
setState('bar', { shallow: false })

Throttling URL updates

Because of browsers rate-limiting the History API, internal 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 would require a throttle of around 340ms. If you end up needing a longer time between updates, you can specify it in the options:

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

// You can also pass the option on calls to setState:
setState('bar', { 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.

Transitions

When combined with shallow: false, you can use the 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:

'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 // opt-in to notify the server (Next.js only)
    })
  )
  // 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>
}

Configuring parsers, default value & options

You can use a builder pattern to facilitate specifying all of those things:

useQueryState(
  'counter',
  parseAsInteger.withDefault(0).withOptions({
    history: 'push',
    shallow: false
  })
)

You can get this pattern for your custom parsers too, and compose them with others:

import { createParser, parseAsHex } from 'nuqs'

// Wrapping your parser/serializer in `createParser`
// gives it access to the builder pattern & server-side
// parsing capabilities:
const hexColorSchema = createParser({
  parse(query) {
    if (query.length !== 6) {
      return null // always return null for invalid inputs
    }
    return {
      // When composing other parsers, they may return null too.
      r: parseAsHex.parse(query.slice(0, 2)) ?? 0x00,
      g: parseAsHex.parse(query.slice(2, 4)) ?? 0x00,
      b: parseAsHex.parse(query.slice(4)) ?? 0x00
    }
  },
  serialize({ r, g, b }) {
    return (
      parseAsHex.serialize(r) +
      parseAsHex.serialize(g) +
      parseAsHex.serialize(b)
    )
  }
})
  // Eg: set common options directly
  .withOptions({ history: 'push' })

// Or on usage:
useQueryState(
  'tribute',
  hexColorSchema.withDefault({
    r: 0x66,
    g: 0x33,
    b: 0x99
  })
)

Note: see this example running in the hex-colors demo.

Multiple Queries (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:

const MultipleQueriesDemo = () => {
  const [lat, setLat] = useQueryState('lat', parseAsFloat)
  const [lng, setLng] = useQueryState('lng', parseAsFloat)
  const randomCoordinates = React.useCallback(() => {
    setLat(Math.random() * 180 - 90)
    setLng(Math.random() * 360 - 180)
  }, [])
}

If you wish to know when the URL has been updated, and what it contains, you can await the Promise returned by the state updater function, which gives you the updated URLSearchParameters object:

const randomCoordinates = React.useCallback(() => {
  setLat(42)
  return setLng(12)
}, [])

randomCoordinates().then((search: URLSearchParams) => {
  search.get('lat') // 42
  search.get('lng') // 12, has been queued and batch-updated
})
Implementation details (Promise caching)

The returned Promise is cached until the next flush to the URL occurs, so all calls to a setState (of any hook) in the same event loop tick will return the same Promise reference.

Due to throttling of calls to the Web History API, the Promise may be cached for several ticks. Batched updates will be merged and flushed once to the URL. This means not every setState will reflect to the URL, if another one comes overriding it before flush occurs.

The returned React state will reflect all set values instantly, to keep UI responsive.


useQueryStates

For query keys that should always move together, you can use useQueryStates with an object containing each key's type:

import { useQueryStates, parseAsFloat } from 'nuqs'

const [coordinates, setCoordinates] = useQueryStates(
  {
    lat: parseAsFloat.withDefault(45.18),
    lng: parseAsFloat.withDefault(5.72)
  },
  {
    history: 'push'
  }
)

const { lat, lng } = coordinates

// Set all (or a subset of) the keys in one go:
const search = await setCoordinates({
  lat: Math.random() * 180 - 90,
  lng: Math.random() * 360 - 180
})

Accessing searchParams in Server Components

If you wish to access the searchParams in a deeply nested Server Component (ie: not in the Page component), you can use createSearchParamsCache to do so in a type-safe manner.

Note: parsers don't validate your data. If you expect positive integers or JSON-encoded objects of a particular shape, you'll need to feed the result of the parser to a schema validation library, like Zod.

// searchParams.ts
import {
  createSearchParamsCache,
  parseAsInteger,
  parseAsString
} from 'nuqs/server'
// Note: import from 'nuqs/server' to avoid the "use client" directive

export const searchParamsCache = createSearchParamsCache({
  // List your search param keys and associated parsers here:
  q: parseAsString.withDefault(''),
  maxResults: parseAsInteger.withDefault(10)
})

// page.tsx
import { searchParamsCache } from './searchParams'

export default function Page({
  searchParams
}: {
  searchParams: Record<string, string | string[] | undefined>
}) {
  // ⚠️ Don't forget to call `parse` here.
  // You can access type-safe values from the returned object:
  const { q: query } = searchParamsCache.parse(searchParams)
  return (
    <div>
      <h1>Search Results for {query}</h1>
      <Results />
    </div>
  )
}

function Results() {
  // Access type-safe search params in children server components:
  const maxResults = searchParamsCache.get('maxResults')
  return <span>Showing up to {maxResults} results</span>
}

The cache will only be valid for the current page render (see React's cache function).

Note: the cache only works for server components, but you may share your parser declaration with useQueryStates for type-safety in client components:

// searchParams.ts
import { parseAsFloat, createSearchParamsCache } from 'nuqs/server'

export const coordinatesParsers = {
  lat: parseAsFloat.withDefault(45.18),
  lng: parseAsFloat.withDefault(5.72)
}
export const coordinatesCache = createSearchParamsCache(coordinatesParsers)

// page.tsx
import { coordinatesCache } from './searchParams'
import { Server } from './server'
import { Client } from './client'

export default async function Page({ searchParams }) {
  await coordinatesCache.parse(searchParams)
  return (
    <>
      <Server />
      <Suspense>
        <Client />
      </Suspense>
    </>
  )
}

// server.tsx
import { coordinatesCache } from './searchParams'

export function Server() {
  const { lat, lng } = coordinatesCache.all()
  // or access keys individually:
  const lat = coordinatesCache.get('lat')
  const lng = coordinatesCache.get('lng')
  return (
    <span>
      Latitude: {lat} - Longitude: {lng}
    </span>
  )
}

// client.tsx
// prettier-ignore
;'use client'

import { useQueryStates } from 'nuqs'
import { coordinatesParsers } from './searchParams'

export function Client() {
  const [{ lat, lng }, setCoordinates] = useQueryStates(coordinatesParsers)
  // ...
}

Serializer helper

To populate <Link> components with state values, you can use the createSerializer helper.

Pass it an object describing your search params, and it will give you a function to call with values, that generates a query string serialized as the hooks would do.

Example:

import {
  createSerializer,
  parseAsInteger,
  parseAsIsoDateTime,
  parseAsString,
  parseAsStringLiteral
} from 'nuqs/server'

const searchParams = {
  search: parseAsString,
  limit: parseAsInteger,
  from: parseAsIsoDateTime,
  to: parseAsIsoDateTime,
  sortBy: parseAsStringLiteral(['asc', 'desc'] as const)
}

// Create a serializer function by passing the description of the search params to accept
const serialize = createSerializer(searchParams)

// Then later, pass it some values (a subset) and render them to a query string
serialize({
  search: 'foo bar',
  limit: 10,
  from: new Date('2024-01-01'),
  // here, we omit `to`, which won't be added
  sortBy: null // null values are also not rendered
})
// ?search=foo+bar&limit=10&from=2024-01-01T00:00:00.000Z

Base parameter

The returned serialize function can take a base parameter over which to append/amend the search params:

serialize('/path?baz=qux', { foo: 'bar' }) // /path?baz=qux&foo=bar

const search = new URLSearchParams('?baz=qux')
serialize(search, { foo: 'bar' }) // ?baz=qux&foo=bar

const url = new URL('https://example.com/path?baz=qux')
serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar

// Passing null removes existing values
serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar

Parser type inference

To access the underlying type returned by a parser, you can use the inferParserType type helper:

import { parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server'

const intNullable = parseAsInteger
const intNonNull = parseAsInteger.withDefault(0)

inferParserType<typeof intNullable> // number | null
inferParserType<typeof intNonNull> // number

For an object describing parsers (that you'd pass to createSearchParamsCache or to useQueryStates, inferParserType will return the type of the object with the parsers replaced by their inferred types:

import { parseAsBoolean, parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server'

const parsers = {
  a: parseAsInteger,
  b: parseAsBoolean.withDefault(false)
}

inferParserType<typeof parsers>
// { a: number | null, b: boolean }

Testing

Since nuqs v2, you can use a testing adapter to unit-test components using useQueryState and useQueryStates in isolation, without needing to mock your framework or router.

Here's an example using Testing Library and Vitest:

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,
    // and give it a function to call on URL updates
    wrapper: ({ children }) => (
      <NuqsTestingAdapter searchParams="?count=42" onUrlUpdate={onUrlUpdate}>
        {children}
      </NuqsTestingAdapter>
    )
  })
  // Initial state assertions: there's a clickable button displaying the count
  const button = screen.getByRole('button')
  expect(button).toHaveTextContent('count is 42')
  // Act
  await user.click(button)
  // 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')
})

See #259 for more testing-related discussions.

Debugging

You can enable debug logs in the browser by setting the debug item in localStorage to nuqs, and reload the page.

// In your devtools:
localStorage.setItem('debug', 'nuqs')

Note: unlike the debug package, this will not work with wildcards, but you can combine it: localStorage.setItem('debug', '*,nuqs')

Log lines will be prefixed with [nuqs] for useQueryState and [nuq+] for useQueryStates, along with other internal debug logs.

User timings markers are also recorded, for advanced performance analysis using your browser's devtools.

Providing debug logs when opening an issue is always appreciated. 🙏

SEO

If your page uses query strings for local-only state, you should add a canonical URL to your page, to tell SEO crawlers to ignore the query string and index the page without it.

In the app router, this is done via the metadata object:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  alternates: {
    canonical: '/url/path/without/querystring'
  }
}

If however the query string is defining what content the page is displaying (eg: YouTube's watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ), your canonical URL should contain relevant query strings, and you can still use useQueryState to read it:

// page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
import { useQueryState } from 'nuqs'
import { parseAsString } from 'nuqs/server'

type Props = {
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata({
  searchParams
}: Props): Promise<Metadata> {
  const videoId = parseAsString.parseServerSide(searchParams.v)
  return {
    alternates: {
      canonical: `/watch?v=${videoId}`
    }
  }
}

Lossy serialization

If your serializer loses precision or doesn't accurately represent the underlying state value, you will lose this precision when reloading the page or restoring state from the URL (eg: on navigation).

Example:

const geoCoordParser = {
  parse: parseFloat,
  serialize: v => v.toFixed(4) // Loses precision
}

const [lat, setLat] = useQueryState('lat', geoCoordParser)

Here, setting a latitude of 1.23456789 will render a URL query string of lat=1.2345, while the internal lat state will be correctly set to 1.23456789.

Upon reloading the page, the state will be incorrectly set to 1.2345.

License

MIT

Made with ❤️ by François Best

Using this package at work ? Sponsor me to help with support and maintenance.

Project analytics and stats

NPM DownloadsLast 30 Days