A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation and automatic state management.
A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation, automatic state management, and intelligent default handling.
# npm
npm install @chronicstone/vue-route-query zod vue-router
# yarn
yarn add @chronicstone/vue-route-query zod vue-router
# pnpm
pnpm add @chronicstone/vue-route-query zod vue-router
# bun
bun add @chronicstone/vue-route-query zod vue-router
import { useRouteQuery } from '@chronicstone/vue-route-query'
import { z } from 'zod'
// Single value with key
const activeLayout = useRouteQuery({
key: 'layout',
schema: z.enum(['table', 'grid']),
default: 'table' // Won't appear in URL when value is 'table'
})
// Type: Ref<'table' | 'grid'>
const filters = useRouteQuery({
schema: {
search: z.string(),
status: z.array(z.string()),
date: z.object({
from: z.string(),
to: z.string()
})
},
default: {
search: '',
status: [],
date: { from: '', to: '' }
}
})
// Type: Ref<{ search: string; status: string[]; date: { from: string; to: string } }>
const userSettings = useRouteQuery({
key: 'settings', // Optional for object schemas - adds root prefix to all properties
schema: {
theme: z.string(),
notifications: z.boolean()
},
default: {
theme: 'light',
notifications: true
}
})
// URL: ?settings.theme=dark&settings.notifications=false
// Without key: ?theme=dark¬ifications=false
const sort = useRouteQuery({
schema: {
key: z.string(),
dir: z.enum(['asc', 'desc'])
},
default: { key: 'id', dir: 'asc' },
nullable: true // Allows the entire object to be null
})
// Type: Ref<{ key: string; dir: 'asc' | 'desc' } | null>
useRouteQuery<Schema, Nullable, Output>(config)
The main composable for managing URL query parameters.
Parameter | Type | Required | Description |
---|---|---|---|
schema |
z.ZodType | Record<string, z.ZodType> |
Yes | Zod schema for validation |
default |
NonNullable<Output> |
Yes | Default value (wonβt appear in URL when active) |
key |
string |
Required for single values | Root key for single value schemas or optional prefix for object schemas |
nullable |
boolean |
No | Whether the entire value can be null |
enabled |
boolean |
No | Enable/disable URL synchronization |
debug |
boolean |
No | Enable debug logging |
mode |
'push' | 'replace' |
No | Navigation mode (default: βreplaceβ) |
Ref<Output>
- A reactive reference to the synchronized state
Default Values: Default values are never shown in the URL. A parameter only appears in the URL when its value differs from the default.
Root Keys for Object Schemas: When using a key
with object schemas, it acts as a prefix for all properties:
// With key
useRouteQuery({ key: 'user', schema: { name: z.string() }, default: { name: '' } })
// URL: ?user.name=John
// Without key
useRouteQuery({ schema: { name: z.string() }, default: { name: '' } })
// URL: ?name=John
Nested Objects: Deep object structures are automatically flattened using dot notation:
// State
{ filters: { date: { from: '2024-01-01' } } }
// URL
?filters.date.from=2024-01-01
Arrays: Arrays are JSON stringified in the URL:
// State
{ tags: ['vue', 'typescript'] }
// URL
?tags=["vue","typescript"]
Multiple Instances: Multiple useRouteQuery
instances with the same key will stay synchronized. However, ensure they use compatible schemas to avoid conflicts.
Schema Validation: Donβt use Zodβs .default()
function - use the default
parameter instead.
Navigation Mode: The mode
parameter controls how router navigation occurs:
'replace'
(default): Updates the URL without creating a new history entry'push'
: Creates a new history entry for each updateWhen multiple instances update simultaneously, if any instance uses 'push'
, the router will use push mode for that batch of updates.
// Using push mode for filters to enable browser back/forward navigation
const filters = useRouteQuery({
schema: {
category: z.string(),
priceRange: z.object({
min: z.number(),
max: z.number()
})
},
default: {
category: '',
priceRange: { min: 0, max: 1000 }
},
mode: 'push' // Each filter change creates a history entry
})
// Using replace mode for preferences (default)
const preferences = useRouteQuery({
schema: {
view: z.enum(['list', 'grid']),
density: z.enum(['compact', 'comfortable'])
},
default: {
view: 'list',
density: 'comfortable'
}
// mode: 'replace' is the default
})
// If both update simultaneously and filters uses 'push',
// the router will use push for that update
const accountSettings = useRouteQuery({
key: 'account', // All properties will be prefixed with 'account.'
schema: {
profile: z.object({
name: z.string(),
email: z.string()
}),
preferences: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean()
})
},
default: {
profile: { name: '', email: '' },
preferences: { theme: 'light', notifications: true }
}
})
// URL structure:
// ?account.profile.name=John&account.profile.email=john@example.com&account.preferences.theme=dark
// Without the key, it would be:
// ?profile.name=John&profile.email=john@example.com&preferences.theme=dark
const filters = useRouteQuery({
schema: {
searchQuery: z.string().optional(),
filters: z.object({
statuses: z.array(z.string()),
categories: z.array(z.string()),
authorizationLabels: z.boolean(),
startDate: z.object({
from: z.string(),
to: z.string()
})
}),
quickFilters: z.record(z.string(), z.any())
},
default: {
searchQuery: '',
filters: {
statuses: [],
categories: [],
authorizationLabels: false,
startDate: { from: '', to: '' }
},
quickFilters: {}
}
})
// URL when changed from default:
// ?searchQuery=test&filters.statuses=["TO_CHECK_EP","VALIDATED"]&filters.categories=["19KZisAzakz3WESKnUy_C"]&filters.authorizationLabels=true&filters.startDate.from=2025-04-23&filters.startDate.to=2025-04-24
const sort = useRouteQuery({
schema: {
key: z.string(),
dir: z.enum(['asc', 'desc'])
},
default: { key: 'createdAt', dir: 'desc' },
nullable: true
})
// Can be set to null to disable sorting
sort.value = null
// URL when null: parameters removed
// URL when default: parameters removed
// URL when custom: ?key=name&dir=asc
const userPreferences = useRouteQuery({
schema: {
theme: z.enum(['light', 'dark', 'system']),
density: z.enum(['compact', 'comfortable', 'spacious']),
notifications: z.object({
email: z.boolean(),
push: z.boolean(),
frequency: z.enum(['instant', 'daily', 'weekly'])
})
},
default: {
theme: 'system',
density: 'comfortable',
notifications: {
email: true,
push: false,
frequency: 'daily'
}
},
enabled: shouldPersistPreferences.value // Conditionally enable URL sync
})
const pagination = useRouteQuery({
schema: {
pageSize: z.number(),
pageIndex: z.number()
},
default: {
pageSize: 20,
pageIndex: 1
},
mode: 'push' // Enable history for pagination
})
// Only appears in URL when different from default
// ?pageSize=50&pageIndex=3
Objects: Nested objects use dot notation
{ user: { settings: { theme: 'dark' } } }
// Becomes: ?user.settings.theme=dark
Arrays: Arrays are JSON stringified
{ tags: ['vue', 'ts'] }
// Becomes: ?tags=["vue","ts"]
Booleans: Represented as string values
{ active: true }
// Becomes: ?active=true
Numbers: Preserved as numeric strings
{ count: 42 }
// Becomes: ?count=42
Null/Undefined: Removed from URL entirely
The library uses a singleton GlobalQueryManager
that:
Works in all modern browsers that support:
The library is written in TypeScript and provides full type inference:
// Inferred type based on schema
const data = useRouteQuery({
schema: {
name: z.string(),
age: z.number().optional()
},
default: { name: '', age: undefined }
})
// data is Ref<{ name: string; age?: number }>
const filters = useRouteQuery({...})
// Reset to default (removes from URL)
filters.value = { ...defaultFilters }
const config = useRouteQuery({
schema: {
advanced: z.boolean(),
// Only used when advanced is true
customSettings: z.object({...}).optional()
},
default: {
advanced: false,
customSettings: undefined
}
})
// Both instances stay in sync
const userSettings1 = useRouteQuery({
key: 'settings',
schema: z.object({...}),
default: {...}
})
const userSettings2 = useRouteQuery({
key: 'settings', // Same key
schema: z.object({...}), // Must be compatible
default: {...}
})
Enable debug mode to see internal operations:
const data = useRouteQuery({
// ... other options
debug: true
})
MIT
Contributions are welcome! Please read our contributing guidelines before submitting a PR.