Skip to main content

What are parsers?

Parsers are the foundation of type safety in nuqs. They define how to convert between URL query string values (always strings) and your application’s typed state values (numbers, booleans, dates, objects, etc.). Every parser implements two core functions:
type SingleParser<T> = {
  // Convert query string to typed value
  parse: (value: string) => T | null
  
  // Convert typed value back to query string
  serialize?: (value: T) => string
  
  // Optional: compare two values for equality
  eq?: (a: T, b: T) => boolean
}
From packages/nuqs/src/parsers.ts:7-32

Why parsers matter

Without parsers, all URL state would be strings:
const [count, setCount] = useQueryState('count')
// count is string | null
// setCount expects string | null

setCount(count + 1) // ❌ Type error: can't add to string
With parsers, you get full type safety:
import { parseAsInteger } from 'nuqs'

const [count, setCount] = useQueryState('count', parseAsInteger)
// count is number | null
// setCount expects number | null

setCount(count ?? 0 + 1) // ✅ Works as expected

Built-in parsers

nuqs provides parsers for common data types:

Primitive types

import {
  parseAsString,    // string (default)
  parseAsInteger,   // number (rounded)
  parseAsFloat,     // number (decimal)
  parseAsBoolean,   // boolean
  parseAsHex        // number (hexadecimal)
} from 'nuqs'

// Basic usage
const [name, setName] = useQueryState('name') // string by default
const [age, setAge] = useQueryState('age', parseAsInteger)
const [price, setPrice] = useQueryState('price', parseAsFloat)
const [enabled, setEnabled] = useQueryState('enabled', parseAsBoolean)
const [color, setColor] = useQueryState('color', parseAsHex) // 0xff00cc

Date and time

import {
  parseAsTimestamp,     // milliseconds since epoch
  parseAsIsoDateTime,   // ISO-8601 with timezone
  parseAsIsoDate        // ISO-8601 date only (YYYY-MM-DD)
} from 'nuqs'

const [created, setCreated] = useQueryState('created', parseAsTimestamp)
// URL: ?created=1704067200000
// State: Date object

const [due, setDue] = useQueryState('due', parseAsIsoDateTime)
// URL: ?due=2024-01-01T00:00:00.000Z

const [date, setDate] = useQueryState('date', parseAsIsoDate)
// URL: ?date=2024-01-01
// Parsed as Date at 00:00:00 UTC
All date parsers use a custom equality function to compare dates by value:
// From packages/nuqs/src/parsers.ts:275-277
function compareDates(a: Date, b: Date) {
  return a.valueOf() === b.valueOf()
}

Enums and literals

import { parseAsStringEnum, parseAsStringLiteral, parseAsNumberLiteral } from 'nuqs'

// String-based enum
enum Direction {
  up = 'UP',
  down = 'DOWN',
  left = 'LEFT',
  right = 'RIGHT'
}

const [direction, setDirection] = useQueryState(
  'direction',
  parseAsStringEnum<Direction>(Object.values(Direction))
    .withDefault(Direction.up)
)
// URL: ?direction=UP
// State: Direction.up

// String literals (with const assertion)
const colors = ['red', 'green', 'blue'] as const

const [color, setColor] = useQueryState(
  'color',
  parseAsStringLiteral(colors).withDefault('red')
)
// Only accepts 'red' | 'green' | 'blue'

// Number literals
const diceSides = [1, 2, 3, 4, 5, 6] as const

const [side, setSide] = useQueryState(
  'side',
  parseAsNumberLiteral(diceSides).withDefault(4)
)
// URL: ?side=4
// State: 1 | 2 | 3 | 4 | 5 | 6

Arrays

import { parseAsArrayOf } from 'nuqs'

// Comma-separated array (default separator)
const [tags, setTags] = useQueryState(
  'tags',
  parseAsArrayOf(parseAsString)
)
// URL: ?tags=react,typescript,nuqs
// State: ['react', 'typescript', 'nuqs']

// Array of integers with custom separator
const [ids, setIds] = useQueryState(
  'ids',
  parseAsArrayOf(parseAsInteger, '|')
)
// URL: ?ids=1|2|3
// State: [1, 2, 3]
Items containing the separator are URI-encoded automatically:
setTags(['hello,world', 'foo']) 
// URL: ?tags=hello%2Cworld,foo
From packages/nuqs/src/parsers.ts:466-510

JSON objects

import { parseAsJson } from 'nuqs'
import { z } from 'zod'

// With Zod validation
const pointSchema = z.object({
  x: z.number(),
  y: z.number()
})

const [point, setPoint] = useQueryState(
  'point',
  parseAsJson(value => pointSchema.parse(value))
)
// URL: ?point=%7B%22x%22%3A10%2C%22y%22%3A20%7D
// State: { x: 10, y: 20 }

// Without validation (use with caution)
type Point = { x: number; y: number }

const [point, setPoint] = useQueryState(
  'point',
  parseAsJson<Point>(value => value as Point)
)
Parsers don’t validate data. Always use a schema validation library like Zod for JSON objects.
The JSON parser includes a custom equality function that compares by value:
// From packages/nuqs/src/parsers.ts:452-455
eq(a, b) {
  // Check for referential equality first
  return a === b || JSON.stringify(a) === JSON.stringify(b)
}

Creating custom parsers

Use createParser to build parsers with full type safety and builder pattern support:
import { createParser } from 'nuqs'

// Hexadecimal color parser
const parseAsHexColor = createParser({
  parse(query: string) {
    if (!/^[0-9A-Fa-f]{6}$/.test(query)) {
      return null // Invalid format
    }
    return `#${query}`
  },
  serialize(value: string) {
    return value.replace('#', '')
  }
})

const [color, setColor] = useQueryState(
  'color',
  parseAsHexColor.withDefault('#ff0000')
)
// URL: ?color=ff0000
// State: '#ff0000'

Parser rules

Always return null for invalid inputs. Never throw errors in the parse function.
// ✅ Good
parse(query: string) {
  const num = parseInt(query)
  return num == num ? num : null // NaN check
}

// ❌ Bad
parse(query: string) {
  return parseInt(query) // Returns NaN for invalid input
}

// ❌ Bad
parse(query: string) {
  if (!/^\d+$/.test(query)) {
    throw new Error('Invalid number') // Don't throw
  }
  return parseInt(query)
}

Composing parsers

You can compose existing parsers to build more complex ones:
import { createParser, parseAsHex } from 'nuqs'

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

const parseAsRgb = createParser({
  parse(query: string) {
    if (query.length !== 6) {
      return null
    }
    return {
      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 }: RGB) {
    return (
      parseAsHex.serialize(r) +
      parseAsHex.serialize(g) +
      parseAsHex.serialize(b)
    )
  }
})

const [color, setColor] = useQueryState(
  'color',
  parseAsRgb.withDefault({ r: 255, g: 0, b: 0 })
)
From the README example.

Parser builder pattern

All parsers created with createParser support a builder pattern for configuration:

Setting defaults

const [count, setCount] = useQueryState(
  'count',
  parseAsInteger.withDefault(0)
)
// count is now number (never null)

setCount(c => c + 1) // No null check needed
setCount(null) // Removes from URL, returns to default (0)
From packages/nuqs/src/parsers.ts:79-103

Setting options

const [query, setQuery] = useQueryState(
  'q',
  parseAsString.withOptions({
    history: 'push',
    shallow: false,
    scroll: true
  })
)

Chaining builders

const [sortBy, setSortBy] = useQueryState(
  'sortBy',
  parseAsStringLiteral(['asc', 'desc'] as const)
    .withDefault('asc')
    .withOptions({ history: 'push' })
)

Type inference

Use inferParserType to extract the TypeScript type from a parser:
import { parseAsInteger, type inferParserType } from 'nuqs'

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

type A = inferParserType<typeof intNullable> // number | null
type B = inferParserType<typeof intNonNull>  // number

// Works with parser objects too
const parsers = {
  a: parseAsInteger,
  b: parseAsBoolean.withDefault(false)
}

type State = inferParserType<typeof parsers>
// { a: number | null, b: boolean }
From packages/nuqs/src/parsers.ts:583-588

Lossless serialization

Serializers must be lossless to avoid data loss on page reload.
// ❌ Bad: loses precision
const geoCoordParser = createParser({
  parse: parseFloat,
  serialize: v => v.toFixed(4) // Rounds to 4 decimal places
})

const [lat, setLat] = useQueryState('lat', geoCoordParser)
setLat(1.23456789)
// URL: ?lat=1.2345
// State: 1.23456789 (in memory)
// After reload: 1.2345 (precision lost!)

// ✅ Good: preserves full precision
const geoCoordParser = createParser({
  parse: parseFloat,
  serialize: String // No rounding
})
From README warning.