Skip to main content

Overview

useQueryStates is a React hook that synchronizes multiple URL query parameters with component state. It’s ideal for managing related query parameters that should always move together, providing atomic updates and type-safe access to multiple state values.

Function Signature

function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
  keyMap: KeyMap,
  options?: Partial<UseQueryStatesOptions<KeyMap>>
): UseQueryStatesReturn<KeyMap>

Type Definitions

type UseQueryStatesKeysMap<Map = any> = {
  [Key in keyof Map]: KeyMapValue<Map[Key]>
}

type KeyMapValue<Type> = GenericParser<Type> & Options & {
  defaultValue?: Type
}

type UseQueryStatesOptions<KeyMap> = Options & {
  urlKeys: UrlKeys<KeyMap>
}

type UseQueryStatesReturn<T extends UseQueryStatesKeysMap> = [
  Values<T>,
  SetValues<T>
]

type Values<T extends UseQueryStatesKeysMap> = {
  [K in keyof T]: T[K]['defaultValue'] extends NonNullable<ReturnType<T[K]['parse']>>
    ? NonNullable<ReturnType<T[K]['parse']>>
    : ReturnType<T[K]['parse']> | null
}

type SetValues<T extends UseQueryStatesKeysMap> = (
  values: Partial<Nullable<Values<T>>> | UpdaterFn<T> | null,
  options?: Options
) => Promise<URLSearchParams>

type UpdaterFn<T extends UseQueryStatesKeysMap> = (
  old: Values<T>
) => Partial<Nullable<Values<T>>> | null

Parameters

keyMap
UseQueryStatesKeysMap
required
An object describing the keys to synchronize and how to parse and serialize them.Each key in the object represents a query parameter, with a parser configuration as its value.
{
  latitude: parseAsFloat.withDefault(45.18),
  longitude: parseAsFloat.withDefault(5.72),
  zoom: parseAsInteger.withDefault(10)
}
options
UseQueryStatesOptions<KeyMap>
Optional configuration object for behavior options.

Behavior Options

history
'push' | 'replace'
default:"'replace'"
How query updates affect page history:
  • 'replace': Keep the current history point (default)
  • 'push': Create a new history entry
scroll
boolean
default:false
Whether to scroll to top after a query state update.
shallow
boolean
default:true
Client-side only updates when true. Set to false to trigger server re-renders (Next.js only).
throttleMs
number
default:50
Maximum time (ms) to wait between URL updates.
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.
clearOnDefault
boolean
default:true
Clear query parameters from the URL when setting to default values.
urlKeys
UrlKeys<KeyMap>
Map state keys to different URL parameter names:
{
  latitude: 'lat',   // Use ?lat=... in URL instead of ?latitude=...
  longitude: 'lng'   // Use ?lng=... in URL instead of ?longitude=...
}

Return Value

Returns a tuple [state, setState] similar to React.useState:
state
Values<KeyMap>
An object containing all state values with keys matching the keyMap:
{
  latitude: number | null,
  longitude: number | null,
  zoom: number
}
Values are null if not in URL and no default is provided, otherwise the parsed or default value.
setState
SetValues<KeyMap>
State updater function accepting:
  1. Partial object with new values:
    setState({ latitude: 48.8566, longitude: 2.3522 })
    
  2. Updater function receiving old state:
    setState(old => ({ zoom: old.zoom + 1 }))
    
  3. null to clear all keys:
    setState(null)
    
Parameters:
  • values: Partial object, updater function, or null
  • options: Optional options to override hook-level settings
Returns: Promise resolving with updated URLSearchParams

Usage Examples

Basic Multi-State Management

'use client'

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

export default function MapView() {
  const [coordinates, setCoordinates] = useQueryStates({
    lat: parseAsFloat.withDefault(45.18),
    lng: parseAsFloat.withDefault(5.72),
    zoom: parseAsInteger.withDefault(10)
  })
  
  const { lat, lng, zoom } = coordinates
  
  return (
    <>
      <div>Latitude: {lat}</div>
      <div>Longitude: {lng}</div>
      <div>Zoom: {zoom}</div>
      
      <button onClick={() => setCoordinates({
        lat: Math.random() * 180 - 90,
        lng: Math.random() * 360 - 180
      })}>
        Random Location
      </button>
    </>
  )
}

Partial Updates

import { useQueryStates, parseAsFloat } from 'nuqs'

export default function Coordinates() {
  const [coords, setCoords] = useQueryStates({
    lat: parseAsFloat,
    lng: parseAsFloat
  })
  
  // Update only latitude
  const updateLat = () => setCoords({ lat: 48.8566 })
  
  // Update only longitude
  const updateLng = () => setCoords({ lng: 2.3522 })
  
  // Update both atomically
  const updateBoth = () => setCoords({ 
    lat: 48.8566, 
    lng: 2.3522 
  })
  
  return (
    <>
      <button onClick={updateLat}>Set Paris Lat</button>
      <button onClick={updateLng}>Set Paris Lng</button>
      <button onClick={updateBoth}>Set Paris</button>
    </>
  )
}

With Updater Function

import { useQueryStates, parseAsInteger } from 'nuqs'

export default function Pagination() {
  const [pagination, setPagination] = useQueryStates({
    page: parseAsInteger.withDefault(1),
    limit: parseAsInteger.withDefault(10)
  })
  
  const nextPage = () => setPagination(old => ({ 
    page: old.page + 1 
  }))
  
  const prevPage = () => setPagination(old => ({ 
    page: Math.max(1, old.page - 1)
  }))
  
  const changeLimit = (newLimit: number) => setPagination({
    limit: newLimit,
    page: 1 // Reset to first page when changing limit
  })
  
  return (
    <>
      <div>Page {pagination.page}</div>
      <div>Showing {pagination.limit} per page</div>
      
      <button onClick={prevPage}>Previous</button>
      <button onClick={nextPage}>Next</button>
      
      <select 
        value={pagination.limit} 
        onChange={e => changeLimit(Number(e.target.value))}
      >
        <option value={10}>10</option>
        <option value={25}>25</option>
        <option value={50}>50</option>
      </select>
    </>
  )
}

With URL Key Mapping

import { useQueryStates, parseAsFloat } from 'nuqs'

export default function Coordinates() {
  // State keys: latitude, longitude
  // URL keys: lat, lng
  const [coords, setCoords] = useQueryStates(
    {
      latitude: parseAsFloat.withDefault(0),
      longitude: parseAsFloat.withDefault(0)
    },
    {
      urlKeys: {
        latitude: 'lat',
        longitude: 'lng'
      }
    }
  )
  
  // Access via state keys
  const { latitude, longitude } = coords
  
  // URL will show: ?lat=45.18&lng=5.72
  return (
    <div>
      Lat: {latitude}, Lng: {longitude}
    </div>
  )
}

Complex Filter State

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

enum SortOrder {
  asc = 'asc',
  desc = 'desc'
}

export default function ProductList() {
  const [filters, setFilters] = useQueryStates({
    search: parseAsString,
    category: parseAsString,
    minPrice: parseAsInteger,
    maxPrice: parseAsInteger,
    tags: parseAsArrayOf(parseAsString),
    sort: parseAsStringEnum<SortOrder>(Object.values(SortOrder))
      .withDefault(SortOrder.asc)
  })
  
  const clearFilters = () => setFilters(null)
  
  const updateSearch = (search: string) => 
    setFilters({ search: search || null })
  
  const updatePriceRange = (min: number, max: number) =>
    setFilters({ minPrice: min, maxPrice: max })
  
  return (
    <>
      <input
        value={filters.search || ''}
        onChange={e => updateSearch(e.target.value)}
        placeholder="Search products..."
      />
      
      <div>
        Price: {filters.minPrice ?? 0} - {filters.maxPrice ?? 1000}
      </div>
      
      <div>Tags: {filters.tags?.join(', ')}</div>
      
      <button onClick={clearFilters}>Clear All Filters</button>
    </>
  )
}

Sharing Parser Definitions

// parsers.ts
import { parseAsFloat, parseAsInteger } from 'nuqs'

export const mapParsers = {
  latitude: parseAsFloat.withDefault(45.18),
  longitude: parseAsFloat.withDefault(5.72),
  zoom: parseAsInteger.withDefault(10)
}

export const mapUrlKeys = {
  latitude: 'lat',
  longitude: 'lng',
  zoom: 'z'
}

// component.tsx
import { useQueryStates } from 'nuqs'
import { mapParsers, mapUrlKeys } from './parsers'

export default function Map() {
  const [coords, setCoords] = useQueryStates(mapParsers, {
    urlKeys: mapUrlKeys
  })
  
  return <div>Map at {coords.latitude}, {coords.longitude}</div>
}

With History Push

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

export default function SearchHistory() {
  const [search, setSearch] = useQueryStates(
    {
      query: parseAsString,
      page: parseAsInteger.withDefault(1)
    },
    {
      history: 'push' // Each update creates history entry
    }
  )
  
  // Users can use browser back/forward to navigate search history
  return (
    <input
      value={search.query || ''}
      onChange={e => setSearch({ 
        query: e.target.value || null,
        page: 1 // Reset to page 1 on new search
      })}
    />
  )
}

With Transitions (Server Updates)

'use client'

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

export default function ServerFilters({ data }) {
  const [isLoading, startTransition] = React.useTransition()
  
  const [filters, setFilters] = useQueryStates(
    {
      search: parseAsString,
      page: parseAsInteger.withDefault(1)
    },
    {
      startTransition,
      shallow: false // Notify the server
    }
  )
  
  if (isLoading) return <div>Loading...</div>
  
  return (
    <input
      value={filters.search || ''}
      onChange={e => setFilters({ 
        search: e.target.value || null,
        page: 1
      })}
    />
  )
}

Awaiting Batched Updates

import { useQueryStates, parseAsFloat } from 'nuqs'

export default function Coordinates() {
  const [coords, setCoords] = useQueryStates({
    lat: parseAsFloat,
    lng: parseAsFloat
  })
  
  const setRandomLocation = async () => {
    const search = await setCoords({
      lat: Math.random() * 180 - 90,
      lng: Math.random() * 360 - 180
    })
    
    // Both updates are applied atomically
    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

Atomic updates: All state changes in a single setState call are applied to the URL together, ensuring consistency.
Partial updates: You can update a subset of keys. Omitted keys retain their current values.
Clearing all state: Passing null to setState clears all managed query parameters from the URL.
URL key mapping: When using urlKeys, always access state using the state key names (from keyMap), not the URL parameter names.
Default values: Keys with default values will never be null in the returned state object.
Cross-hook synchronization: Multiple useQueryState or useQueryStates hooks managing the same URL parameters will stay synchronized automatically.

Common Patterns

Conditional Updates

const updateFilters = (search?: string, category?: string) => {
  const updates: Partial<typeof filters> = {}
  if (search !== undefined) updates.search = search || null
  if (category !== undefined) updates.category = category || null
  setFilters(updates)
}

Reset Individual Keys

// Reset only the search query
setFilters({ search: null })

// Reset multiple specific keys
setFilters({ search: null, category: null })

Override Options Per Update

const [state, setState] = useQueryStates(parsers, { history: 'push' })

// Override on individual calls
setState({ key: 'value' }, { history: 'replace' })

Comparison with useQueryState

FeatureuseQueryStateuseQueryStates
Query parametersSingleMultiple
UpdatesOne at a timeAtomic batch updates
Return type[value, setValue][values, setValues]
Type inferenceSimpleObject with typed keys
Best forIndependent parametersRelated parameters

See Also