Type-safe URL query state management for Vue 3
npm install vuerl


> Type-safe URL query state management for Vue 3 + Vue Router.
This package gives you typed composables that keep Vue state and the URL query string in sync. You get the same ergonomics as const [value, setValue] = useQueryState(...) in React/nuqs, including parser-based type safety, automatic batching, and browser-aware rate limiting.
``bash`
pnpm add vuerlor
npm install vuerlor
yarn add vuerl
Peer dependencies:
`bash`
pnpm add vue@^3.3 vue-router@^4.0
`ts
`
- State updates immediately when you type.
- URL updates are debounced to keep browsers happy.
- Defaults never show up in the URL (unless you turn off clearOnDefault).search.value
- TypeScript knows is string and page.value is number.
Use useQueryStates when several params should update together.
`ts
import {
useQueryStates,
parseAsString,
parseAsInteger,
parseAsStringLiteral
} from 'vuerl'
const [filters, setFilters] = useQueryStates({
search: parseAsString.withDefault(''),
status: parseAsStringLiteral(['active', 'inactive']).withDefault('active'),
limit: parseAsInteger.withDefault(20)
}, {
debounce: 120,
history: 'push'
})
setFilters({ search: 'vue' }) // single field
setFilters({ status: 'inactive', limit: 50 }) // batched update → one router push
setFilters({ status: null }) // drop from URL + reset to parser default
setFilters(null) // reset every field to defaults
`
Updates inside the same tick (setFilters({...}); setFilters({...})) merge before touching the router, so you only pay for one navigation.
Parsers define how a query string turns into a typed value (and back). Every parser supports .withDefault(value) and .withOptions({ ...queryStateOptions }).
| Parser | Example | URL → Value |
| --- | --- | --- |
| parseAsString | "hello" | 'hello'parseAsInteger
| | "42" | 42parseAsFloat
| | "3.14" | 3.14parseAsBoolean
| | "true" | trueparseAsStringLiteral(['asc','desc'])
| | "asc" | 'asc'parseAsStringEnum(MyEnum)
| | "VALUE" | MyEnum.VALUEparseAsArrayOf(parseAsInteger)
| | "1,2,3" | [1,2,3]parseAsNativeArrayOf(parseAsString)
| | ?tag=a&tag=b | ['a','b']parseAsIsoDate
| | "2025-11-22" | DateparseAsIsoDateTime
| | "2025-11-22T10:30:00Z" | DateparseAsJson()
| | "%7B%5C"id%5C":1%7D" | { id: 1 }parseAsHex
| | "ff00ff" | 'ff00ff'withDefault(customParser, defaultValue)
| | – | Keeps custom parser logic but adds defaults |
Need something custom? Implement the Parser interface or wrap an existing parser with .withDefault/.withOptions.
`ts`
const parseAsSlug = withDefault({
parse: (value) => (value && /[a-z0-9-]+/.test(value) ? value : null),
serialize: (value) => value ?? null
}, 'home')
Both hooks accept the same QueryStateOptions either directly or via parser .withOptions.
| Option | Default | What it does |
| --- | --- | --- |
| history | 'replace' | 'push' to record every change in browser history. |debounce
| | browser-safe (50ms / 120ms Safari) | Delay before writing to the URL. |throttle
| | null | Alternate rate limiter if you prefer throttling. |clearOnDefault
| | true | Drop params from the URL when they match parser default. |shallow
| | true | Skip full router navigation (like router.replace({ query })). Set false when SSR needs to know about query changes. |scroll
| | false | Force scroll to top on navigation. |
Parser-level options merge with hook options, so you can keep most defaults global and override only the odd field:
`ts
const parser = parseAsInteger
.withDefault(20)
.withOptions({ clearOnDefault: false })
const [, setLimit] = useQueryState('limit', parser)
`
Both hooks watch route.query so back/forward navigation, shared URLs, or manual router.push calls stay in sync automatically. Pending debounced updates are cancelled when the user navigates elsewhere.
parseAsArrayOf (comma separated) and parseAsNativeArrayOf (repeated params) always return concrete arrays:
`ts
const [tags] = useQueryState('tags', parseAsArrayOf(parseAsString))
tags.value // always string[]
const [, setIds] = useQueryState('id', parseAsNativeArrayOf(parseAsInteger))
setIds([1, 2, 3]) // → ?id=1&id=2&id=3
`
If your parser returns objects/arrays that shouldn't trigger updates when reference changes, provide eq(a, b):
`ts`
const parseAsJsonFilters = withDefault({
parse: (value) => value ? JSON.parse(value) : null,
serialize: (value) => value ? JSON.stringify(value) : null,
eq: (a, b) => JSON.stringify(a) === JSON.stringify(b)
}, {})
The test suite uses Vue Test Utils + Vue Router in memory. If you need to write your own tests, copy the helper from tests/helpers.ts to mount a composable inside a dummy component.
Does this work with SSR?
Yes. Hooks guard against window access and fall back to safe defaults in SSR environments. Set { shallow: false } if your framework needs full navigations for query changes.
What about browser rate limits?
We auto-detect Safari vs other browsers and ensure the debounce/throttle never drops below 120ms/50ms respectively. You can still pass your own values—they're clamped to the safe floor.
Can I mix useQueryState and useQueryStates?
Absolutely. They both watch the same router instance and will stay in sync. When both write the same key the last writer wins, so prefer one hook per param to avoid confusion.
PRs welcome! If you add new parsers, remember to extend src/parsers.ts, export from src/index.ts, and cover them in tests/parsers.test.ts`.
MIT