vue-route-query
vue-route-query copied to clipboard
A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation and automatic state management.
@chronicstone/vue-route-query
A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation, automatic state management, and intelligent default handling.
Features
- ๐ Type-safe: Full TypeScript support with Zod schema validation
- ๐ Bidirectional sync: Automatic synchronization between component state and URL
- ๐ฏ Deep object support: Handles nested objects and arrays with proper serialization
- โก Performance optimized: Batched updates with async processing
- ๐งน Smart cleanup: Automatically removes default values from URL
- ๐ Vue Router integration: Seamless integration with Vue Router
- ๐จ Flexible API: Support for single values or complex objects
- ๐ Nested path support: Deep object structures automatically transformed to dot notation
- ๐ Instance sync: Multiple instances with the same key stay synchronized
- ๐ History control: Choose between push and replace modes for router navigation
Table of Contents
- @chronicstone/vue-route-query
- Features
- Table of Contents
- Installation
- Basic Usage
- Single Value
- Object Schema
- Object Schema with Root Key
- Nullable Schema
- API Reference
useRouteQuery<Schema, Nullable, Output>(config)- Parameters
- Returns
- Important Behavior Notes
- Advanced Usage
- Navigation Mode Control
- Object Schema with Root Key Prefix
- Complex Filtering System
- Sortable Table with Nullable State
- Dynamic Schema with Persistence Control
- Pagination with Type Safety
- Under the Hood
- State Management Lifecycle
- URL Transformation Rules
- Global Query Manager
- Performance Considerations
- Browser Support
- TypeScript Support
- Common Patterns
- Resetting to Defaults
- Conditional Parameters
- Synchronized Instances
- Troubleshooting
- Common Issues
- Debug Mode
- License
- Contributing
Installation
# 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
Basic Usage
Single Value
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'>
Object Schema
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 } }>
Object Schema with Root Key
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
Nullable Schema
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>
API Reference
useRouteQuery<Schema, Nullable, Output>(config)
The main composable for managing URL query parameters.
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') |
Returns
Ref<Output> - A reactive reference to the synchronized state
Important Behavior Notes
-
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
keywith 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
useRouteQueryinstances 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 thedefaultparameter instead. -
Navigation Mode: The
modeparameter controls how router navigation occurs:'replace'(default): Updates the URL without creating a new history entry'push': Creates a new history entry for each update
When multiple instances update simultaneously, if any instance uses
'push', the router will use push mode for that batch of updates.
Advanced Usage
Navigation Mode Control
// 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
Object Schema with Root Key Prefix
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&[email protected]&account.preferences.theme=dark
// Without the key, it would be:
// ?profile.name=John&[email protected]&preferences.theme=dark
Complex Filtering System
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
Sortable Table with Nullable State
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
Dynamic Schema with Persistence Control
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
})
Pagination with Type Safety
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
Under the Hood
State Management Lifecycle
- Initialization: The composable initializes with either URL values (if present) or default values
- Synchronization: Changes to the ref automatically update the URL, and URL changes update the ref
- Cleanup: When values match defaults, they're removed from the URL
- Batching: Multiple rapid updates are batched and processed in the next tick
URL Transformation Rules
-
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
Global Query Manager
The library uses a singleton GlobalQueryManager that:
- Batches multiple updates to prevent race conditions
- Processes all updates in the next tick
- Ensures consistent state across all instances
- Handles cleanup of removed properties
- Intelligently combines navigation modes (push if any instance requests push)
Performance Considerations
- Batching: All updates are batched to minimize router operations
- Shallow Comparison: Uses shallow comparison for primitives
- Deep Comparison: Uses recursive comparison for objects only when needed
- URL Size: Be mindful of browser URL length limits with large data structures
Browser Support
Works in all modern browsers that support:
- Vue 3
- URLSearchParams API
- ES2015+
TypeScript 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 }>
Common Patterns
Resetting to Defaults
const filters = useRouteQuery({...})
// Reset to default (removes from URL)
filters.value = { ...defaultFilters }
Conditional Parameters
const config = useRouteQuery({
schema: {
advanced: z.boolean(),
// Only used when advanced is true
customSettings: z.object({...}).optional()
},
default: {
advanced: false,
customSettings: undefined
}
})
Synchronized Instances
// 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: {...}
})
Troubleshooting
Common Issues
- Schema Mismatch: Ensure multiple instances with the same key use compatible schemas
- Default Values: Remember that default values never appear in the URL
- Type Errors: Use proper TypeScript types when working with refs
- Performance: For large data structures, consider pagination or filtering
- Navigation Conflicts: When using mixed modes, push takes precedence over replace
Debug Mode
Enable debug mode to see internal operations:
const data = useRouteQuery({
// ... other options
debug: true
})
License
MIT
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.