Skip to main content

Overview

useQueryState is a React hook that synchronizes a single URL query parameter with component state. It works like useState, but the state is stored in the URL query string and persists across page reloads and browser navigation.

Function Signature

function useQueryState<T = string>(
  key: string,
  options?: UseQueryStateOptions<T>
): UseQueryStateReturn<T, DefaultValue>

Type Definitions

type UseQueryStateOptions<T> = GenericParser<T> & Options

type UseQueryStateReturn<Parsed, Default> = [
  Default extends undefined ? Parsed | null : Parsed,
  (
    value: Parsed | null | ((old: Parsed | null) => Parsed | null),
    options?: Options
  ) => Promise<URLSearchParams>
]

Parameters

key
string
required
The URL query string key to bind to. This will be the parameter name in the URL.Example: 'tag' will manage the tag parameter in ?tag=value
options
UseQueryStateOptions<T>
Configuration object combining parser options and behavior options.

Parser Options

parse
(value: string) => T | null
Function to convert a query string value into a state value. Return null for invalid inputs.
serialize
(value: T) => string
Function to render the state value into a query string value. Defaults to String().
defaultValue
T
Default value to return when the query parameter is not present in the URL. Makes the returned state non-nullable.
eq
(a: T, b: T) => boolean
Equality comparison function. Used with clearOnDefault to determine if a value equals the default.

Behavior Options

history
'push' | 'replace'
default:"'replace'"
How the query update affects page history:
  • 'replace': Keep the current history point and only replace the query string (default)
  • 'push': Create a new history entry, allowing use of back/forward buttons
scroll
boolean
default:false
Whether to scroll to top after a query state update.
shallow
boolean
default:true
Client-side only updates when true (default). Set to false to trigger server re-renders (Next.js only).
throttleMs
number
default:50
Maximum time (ms) to wait between URL updates. Minimum value is 50ms. Safari may require ~120ms.
Deprecated: Use limitUrlUpdates instead.
limitUrlUpdates
LimitUrlUpdates
Rate limiting configuration for URL updates:
type LimitUrlUpdates = 
  | { method: 'throttle'; timeMs: number }
  | { method: 'debounce'; timeMs: number }
startTransition
TransitionStartFunction
Pass startTransition from React.useTransition() to observe loading states during server updates.
clearOnDefault
boolean
default:true
Clear the query parameter from the URL when setting state to the default value.

Return Value

Returns a tuple [state, setState] similar to React.useState:
state
T | null
The current state value:
  • null if the query parameter is not in the URL (when no default value is provided)
  • The parsed value from the URL
  • The default value if provided and query parameter is missing
setState
function
State updater function with the signature:
(
  value: T | null | ((old: T | null) => T | null),
  options?: Options
) => Promise<URLSearchParams>
Parameters:
  • value: New state value, null to clear, or updater function
  • options: Optional options to override hook-level settings
Returns: Promise that resolves with the updated URLSearchParams after the URL is updated

Usage Examples

Basic String State

'use client'

import { useQueryState } from 'nuqs'

export default function SearchFilter() {
  const [tag, setTag] = useQueryState('tag')
  
  return (
    <>
      <h1>Filter by tag: {tag || 'all'}</h1>
      <input 
        value={tag || ''} 
        onChange={e => setTag(e.target.value || null)} 
      />
      <button onClick={() => setTag(null)}>Clear</button>
    </>
  )
}

With Type Parser

import { useQueryState, parseAsInteger } from 'nuqs'

export default function Counter() {
  const [count, setCount] = useQueryState('count', parseAsInteger)
  
  return (
    <>
      <pre>count: {count ?? 'null'}</pre>
      <button onClick={() => setCount(c => (c ?? 0) + 1)}>+</button>
      <button onClick={() => setCount(c => (c ?? 0) - 1)}>-</button>
      <button onClick={() => setCount(null)}>Clear</button>
    </>
  )
}

With Default Value

import { useQueryState, parseAsInteger } from 'nuqs'

export default function Counter() {
  const [count, setCount] = useQueryState(
    'count',
    parseAsInteger.withDefault(0)
  )
  
  // count is never null when default value is provided
  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(null) // Clears from URL, returns 0
  
  return (
    <>
      <pre>count: {count}</pre>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </>
  )
}

With History Push

import { useQueryState } from 'nuqs'

export default function SearchHistory() {
  const [query, setQuery] = useQueryState('q', { 
    history: 'push' 
  })
  
  // Each update creates a new history entry
  // Use browser back button to navigate search history
  return (
    <input
      value={query || ''}
      onChange={e => setQuery(e.target.value || null)}
      placeholder="Search..."
    />
  )
}

With Custom Parser

import { useQueryState } from 'nuqs'

type RGB = { r: number; g: number; b: number }

export default function ColorPicker() {
  const [color, setColor] = useQueryState<RGB>('color', {
    parse: (query: string) => {
      const [r, g, b] = query.split(',').map(Number)
      if (!r || !g || !b) return null
      return { r, g, b }
    },
    serialize: ({ r, g, b }) => `${r},${g},${b}`,
    defaultValue: { r: 255, g: 255, b: 255 }
  })
  
  return (
    <div style={{ backgroundColor: `rgb(${color.r},${color.g},${color.b})` }}>
      <input 
        type="range" 
        value={color.r} 
        onChange={e => setColor(c => ({ ...c, r: Number(e.target.value) }))}
      />
    </div>
  )
}

With Transitions (Server Updates)

'use client'

import React from 'react'
import { useQueryState, parseAsString } from 'nuqs'

export default function ServerSearch({ data }) {
  const [isLoading, startTransition] = React.useTransition()
  const [query, setQuery] = useQueryState(
    'query',
    parseAsString.withOptions({
      startTransition,
      shallow: false // Notify the server
    })
  )
  
  if (isLoading) return <div>Loading...</div>
  
  return (
    <input
      value={query || ''}
      onChange={e => setQuery(e.target.value || null)}
    />
  )
}

Awaiting URL Updates

import { useQueryState, parseAsFloat } from 'nuqs'

export default function Coordinates() {
  const [lat, setLat] = useQueryState('lat', parseAsFloat)
  const [lng, setLng] = useQueryState('lng', parseAsFloat)
  
  const setRandomLocation = async () => {
    setLat(Math.random() * 180 - 90)
    const search = await setLng(Math.random() * 360 - 180)
    
    // Both updates are batched and applied together
    console.log('Updated URL:', search.toString())
    console.log('Lat:', search.get('lat'))
    console.log('Lng:', search.get('lng'))
  }
  
  return <button onClick={setRandomLocation}>Random Location</button>
}

Behavior Notes

Default values are internal: When you specify a defaultValue, it will be returned when the query parameter is missing, but it won’t be written to the URL.
Clearing state: Setting the state to null removes the key from the query string. If a default value is provided, the state will return to that default.
Invalid parse results: If your parse function returns null for an invalid value in the URL, the state will be null (or the default value if provided).
State updates are batched: Multiple setState calls in the same event loop tick are merged and applied to the URL together.
URL updates are throttled: To prevent browser rate-limiting of the History API, URL updates are throttled (default: 50ms). The returned state updates immediately for responsive UI.

Common Patterns

Override Options Per Update

const [state, setState] = useQueryState('key', { history: 'push' })

// Override on individual calls
setState(null, { history: 'replace' }) // Replaces instead of pushing

Builder Pattern Configuration

import { parseAsInteger } from 'nuqs'

const [count, setCount] = useQueryState(
  'count',
  parseAsInteger
    .withDefault(0)
    .withOptions({
      history: 'push',
      shallow: false,
      throttleMs: 100
    })
)

See Also