A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation and automatic state management.
- star 数
- 36
- watcher 数
- 1
- fork 数
- 3
- issue 数
- 2
- 最終更新
- 2025/08/21
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>
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.