Skip to main content
This guide will walk you through creating a simple search interface with nuqs. You’ll learn the core concepts while building something practical.
Make sure you’ve completed the Installation steps before continuing.

Basic Usage: Search Input

Let’s create a search component that stores the query in the URL.
1

Create your component

Create a new client component (for Next.js app router, mark it with 'use client'):
components/Search.tsx
'use client' // Only needed for Next.js App Router

import { useQueryState } from 'nuqs'

export default function Search() {
  const [search, setSearch] = useQueryState('q')

  return (
    <div>
      <input
        type="text"
        value={search || ''}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={() => setSearch(null)}>Clear</button>
      {search && <p>Searching for: {search}</p>}
    </div>
  )
}
The useQueryState hook works just like useState, but syncs with the URL query string!
2

Try it out

Add the component to your page and start typing. Watch the URL update in real-time:
  • Type “react” → URL becomes ?q=react
  • Click “Clear” → URL becomes / (query removed)
  • Refresh the page → Your input persists!
The URL is the source of truth. When you reload the page, the state is restored from the URL automatically.

Adding Type Safety with Parsers

Let’s add pagination with proper number parsing:
1

Import parsers

nuqs provides built-in parsers for common types:
components/Pagination.tsx
'use client'

import { useQueryState, parseAsInteger } from 'nuqs'

export default function Pagination() {
  const [page, setPage] = useQueryState(
    'page',
    parseAsInteger.withDefault(1)
  )

  return (
    <div>
      <p>Current page: {page}</p>
      <button
        onClick={() => setPage(page - 1)}
        disabled={page <= 1}
      >
        Previous
      </button>
      <button onClick={() => setPage(page + 1)}>
        Next
      </button>
      <button onClick={() => setPage(1)}>
        Reset to page 1
      </button>
    </div>
  )
}
Setting page to 1 (the default value) will clear the query from the URL automatically.
2

Benefits of parsers

  • Type safety: page is always a number, never null
  • Validation: Invalid values are rejected and fall back to the default
  • Serialization: Numbers are properly converted to/from URL strings

Managing Multiple Query States

Use useQueryStates to manage related queries together:
'use client'

import { useQueryStates, parseAsInteger, parseAsStringLiteral } from 'nuqs'

const sortOrders = ['asc', 'desc'] as const

export default function Filter() {
  const [filters, setFilters] = useQueryStates({
    search: {
      type: 'single',
      parse: (value) => value,
      serialize: (value) => value,
      eq: (a, b) => a === b
    },
    page: parseAsInteger.withDefault(1),
    sort: parseAsStringLiteral(sortOrders).withDefault('asc')
  })

  return (
    <div>
      <input
        type="text"
        value={filters.search || ''}
        onChange={(e) => setFilters({
          search: e.target.value,
          page: 1 // Reset to page 1 when searching
        })}
        placeholder="Search..."
      />

      <select
        value={filters.sort}
        onChange={(e) => setFilters({
          sort: e.target.value as 'asc' | 'desc',
          page: 1
        })}
      >
        <option value="asc">Ascending</option>
        <option value="desc">Descending</option>
      </select>

      <div>
        <p>Page: {filters.page}</p>
        <button onClick={() => setFilters({ page: filters.page - 1 })}>
          Previous
        </button>
        <button onClick={() => setFilters({ page: filters.page + 1 })}>
          Next
        </button>
      </div>

      <button onClick={() => setFilters(null)}>
        Reset all filters
      </button>
    </div>
  )
}
Multiple state updates in the same event are automatically batched into a single URL update.

Next Steps

You now know the basics of nuqs! Here’s what to explore next:

Parsers

Learn about all built-in parsers and how to create custom ones

Options

Control history mode, shallow routing, and throttling

Server-Side

Use nuqs with Server Components and server-side rendering

Testing

Learn how to test components that use nuqs

Common Patterns

Debouncing Input

For search inputs, you might want to debounce URL updates:
import { useQueryState } from 'nuqs'

const [search, setSearch] = useQueryState('q', {
  throttleMs: 500 // Wait 500ms before updating the URL
})

History Mode

By default, URL updates replace the current history entry. To enable Back button navigation:
import { useQueryState, parseAsInteger } from 'nuqs'

const [page, setPage] = useQueryState(
  'page',
  parseAsInteger.withDefault(1).withOptions({
    history: 'push' // Append to history instead of replacing
  })
)

Server Updates (Next.js)

By default, updates are client-only (shallow). To trigger server component re-renders:
import { useQueryState } from 'nuqs'

const [filter, setFilter] = useQueryState('filter', {
  shallow: false // Notify server components of changes
})
Setting shallow: false will cause server components to re-render, which can be slower. Only use this when you need server-side updates.

Complete Example

Here’s a complete example combining everything:
app/page.tsx
'use client'

import { useQueryStates, parseAsInteger, parseAsString } from 'nuqs'

export default function ProductList() {
  const [params, setParams] = useQueryStates({
    search: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    limit: parseAsInteger.withDefault(10)
  })

  // In a real app, you'd fetch data based on these params
  const { search, page, limit } = params

  return (
    <div>
      <h1>Products</h1>

      <input
        type="text"
        value={search}
        onChange={(e) => setParams({
          search: e.target.value,
          page: 1
        })}
        placeholder="Search products..."
      />

      <select
        value={limit}
        onChange={(e) => setParams({
          limit: parseInt(e.target.value),
          page: 1
        })}
      >
        <option value="10">10 per page</option>
        <option value="25">25 per page</option>
        <option value="50">50 per page</option>
      </select>

      {/* Your product list here */}
      <div className="results">
        <p>Page {page}, showing {limit} items</p>
        {search && <p>Searching for: {search}</p>}
      </div>

      <div className="pagination">
        <button
          onClick={() => setParams({ page: page - 1 })}
          disabled={page <= 1}
        >
          Previous
        </button>
        <span>Page {page}</span>
        <button onClick={() => setParams({ page: page + 1 })}>
          Next
        </button>
      </div>

      <button onClick={() => setParams(null)}>
        Clear all filters
      </button>
    </div>
  )
}
This creates a URL like: ?search=laptop&page=2&limit=25

Build docs developers (and LLMs) love