Vue 3 context storage system with URL query synchronization support
npm install vue-context-storageVue 3 context storage system with URL query, localStorage, and sessionStorage synchronization support.






!CI
!Coverage


A powerful state management solution for Vue 3 applications that provides:
- Context-based storage using Vue's provide/inject API
- Automatic URL query synchronization for preserving state across page reloads
- localStorage & sessionStorage handlers for persistent and session-scoped state
- Multiple storage contexts with activation management
- Type-safe TypeScript support
- Tree-shakeable and lightweight
š Try the interactive playground
``bash`
npm install vue-context-storage
- ā
Vue 3 Composition API - Built with modern Vue patterns
- ā
URL Query Sync - Automatically sync state with URL parameters
- ā
localStorage Handler - Persist state to localStorage with cross-tab sync
- ā
sessionStorage Handler - Session-scoped state that survives page refreshes
- ā
Multiple Contexts - Support multiple independent storage contexts
- ā
TypeScript - Full type safety and IntelliSense support
- ā
Flexible - Works with vue-router 4+ or 5+
- ā
Transform Helpers - Built-in utilities for type conversion
In Vue applications, reactive state often needs to live beyond a single component. Filters, pagination, sorting, and user preferences must survive page reloads, be shareable via URL, or persist across sessions. Solving this typically means writing the same boilerplate over and over: manually reading and writing query parameters with vue-router, serializing objects to localStorage, handling type coercion from URL strings, and keeping everything in sync.
vue-context-storage eliminates that repetitive work. You declare your reactive state once, point it at a storage target, and the library handles the rest:
- URL query parameters stay in sync with your data automatically -- users can bookmark or share a page and get the exact same state back.
- localStorage and sessionStorage are kept up to date without manual getItem/setItem calls, including cross-tab synchronization.
- Type safety is preserved end-to-end: URL strings are coerced back to numbers, booleans, and arrays via transform helpers or Zod schemas.
- Multiple independent contexts (e.g. two data tables on the same page) are supported out of the box through the prefix pattern, so query parameters never collide.
The goal is a single, declarative API -- useContextStorage('query', data, options) -- that replaces scattered watchers, router guards, and storage listeners with one composable call per piece of state.
Import ContextStorage component in your App.vue:
`vue
`
Register the plugin in your main app file:
`typescript
import { createApp } from 'vue'
import { VueContextStoragePlugin } from 'vue-context-storage/plugin'
import App from './App.vue'
const app = createApp(App)
// Register components globally
app.use(VueContextStoragePlugin)
app.mount('#app')
`
Then use components without importing in your App.vue:
`vue`
useContextStorage() provides a single entry point for all handler types:
`vue`
Options are type-checked per handler ā 'query' accepts query options, 'localStorage' and 'sessionStorage' require a key, etc.
You can also pass an injection key directly instead of a string:
`typescript
import { contextStorageQueryHandlerInjectKey } from 'vue-context-storage'
useContextStorage(contextStorageQueryHandlerInjectKey, filters, {
prefix: 'filters',
})
`
Register your own handlers at runtime and extend the type map for full type safety:
`typescript
import { defineContextStorageHandler } from 'vue-context-storage'
import { myHandlerInjectionKey } from './my-handler'
// Runtime registration
defineContextStorageHandler('myHandler', myHandlerInjectionKey)
// TypeScript augmentation (e.g. in a .d.ts or at module level)
declare module 'vue-context-storage' {
interface ContextStorageHandlerMap {
myHandler: { key: string }
}
}
// Now fully type-checked
useContextStorage('myHandler', data, { key: 'example' })
`
The component adds a prefix to all useContextStorage calls within its subtree. Prefixes stack when nested, and are concatenated with bracket notation.
`vue`
Inside MyTable, any useContextStorage('query', data) call will automatically get prefix: 'tables'. If the composable also specifies its own prefix, they are combined:
`typescript`
// Inside MyTable ā effective prefix becomes 'table[filters]'
useContextStorage('query', filters, { prefix: 'filters' })
// URL: ?table[filters][search]=...
Nested components stack their prefixes:
`vue`
Pass an object to apply different prefixes per handler type:
`vue`
When the name prop changes, all descendant components are re-created and re-registered with the new prefix:
`vue`
Sync reactive state with URL query parameters:
`vue`
Also available as a dedicated composable:
`typescript
import { useContextStorageQueryHandler } from 'vue-context-storage'
useContextStorageQueryHandler(filters, {
prefix: 'filters',
})
`
Convert URL query string values to proper types:
`typescript
import { ref } from 'vue'
import { useContextStorage, transform } from 'vue-context-storage'
interface TableState {
page: number
search: string
perPage: number
}
const state = ref
page: 1,
search: '',
perPage: 25,
})
useContextStorage('query', state, {
prefix: 'table',
transform: (deserialized, initial) => ({
page: transform.asNumber(deserialized.page, { fallback: 1 }),
search: transform.asString(deserialized.search, { fallback: '' }),
perPage: transform.asNumber(deserialized.perPage, { fallback: 25 }),
}),
})
`
- asNumber(value, options) - Convert to numberasString(value, options)
- - Convert to stringasBoolean(value, options)
- - Convert to booleanasArray(value, options)
- - Convert to arrayasNumberArray(value, options)
- - Convert to number array
Alternatively, you can use Zod schemas for automatic validation and type inference:
`typescript
import { z } from 'zod'
import { useContextStorage } from 'vue-context-storage'
// Define schema with automatic coercion
const FiltersSchema = z.object({
search: z.string().default(''),
page: z.coerce.number().int().positive().default(1),
status: z.enum(['active', 'inactive']).default('active'),
})
const filters = ref(FiltersSchema.parse({}))
// Use schema for automatic validation
useContextStorage('query', filters, {
prefix: 'filters',
schema: FiltersSchema,
})
`
Benefits:
- Automatic type coercion (strings ā numbers, etc.)
- Runtime validation with detailed errors
- Automatic TypeScript type inference
- Less boilerplate code
- Single source of truth for structure and validation
Keep empty state in URL to prevent resetting on reload:
`typescript`
useContextStorage('query', filters, {
prefix: 'filters',
preserveEmptyState: true,
// Empty filters will show as: ?filters
// Without this option, empty filters would clear the URL completely
})
Customize global behavior:
`typescript
import { ContextStorageQueryHandler } from 'vue-context-storage'
ContextStorageQueryHandler.configure({
mode: 'push', // 'replace' (default) or 'push' for history
preserveUnusedKeys: true, // Keep other query params
preserveEmptyState: false,
})
`
Persist reactive state to localStorage. Data is automatically synced across browser tabs.
`vue`
Also available as a dedicated composable:
`typescript
import { useContextStorageLocalStorage } from 'vue-context-storage'
useContextStorageLocalStorage(settings, {
key: 'app-settings',
})
`
`typescript
import { ContextStorageLocalStorageHandler } from 'vue-context-storage'
ContextStorageLocalStorageHandler.configure({
listenToStorageEvents: true, // Cross-tab sync (default: true)
})
`
Persist reactive state to sessionStorage. Data survives page refreshes but is cleared when the tab is closed.
`vue`
Also available as a dedicated composable:
`typescript
import { useContextStorageSessionStorage } from 'vue-context-storage'
useContextStorageSessionStorage(formDraft, {
key: 'contact-form-draft',
})
`
The prefix is appended to the storage key in bracket notation, so each prefixed registration gets its own storage entry:
`typescript
const filters = reactive({ search: '', status: 'active' })
useContextStorage('sessionStorage', filters, {
key: 'app-state',
prefix: 'filters', // Storage key: 'app-state[filters]', value: { search: '', status: 'active' }
})
const pagination = reactive({ page: 1, perPage: 25 })
useContextStorage('sessionStorage', pagination, {
key: 'app-state',
prefix: 'pagination', // Storage key: 'app-state[pagination]', value: { page: 1, perPage: 25 }
})
`
Convert stored values to proper types when reading from storage:
`typescript
import { useContextStorage, transform } from 'vue-context-storage'
const settings = reactive({
theme: 'light',
fontSize: 14,
})
useContextStorage('localStorage', settings, {
key: 'app-settings',
transform: (deserialized, initial) => ({
theme: transform.asString(deserialized.theme, { fallback: 'light' }),
fontSize: transform.asNumber(deserialized.fontSize, { fallback: 14 }),
}),
})
`
`typescript
import { z } from 'zod'
import { useContextStorageLocalStorage } from 'vue-context-storage'
const SettingsSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
fontSize: z.number().int().positive().default(14),
sidebarOpen: z.boolean().default(true),
})
const settings = reactive(SettingsSchema.parse({}))
useContextStorage('localStorage', settings, {
key: 'app-settings',
schema: SettingsSchema,
})
`
Provide custom serializer/deserializer functions:
`typescript`
useContextStorage('localStorage', settings, {
key: 'app-settings',
serializer: (data) => btoa(JSON.stringify(data)),
deserializer: (str) => JSON.parse(atob(str)),
})
#### useContextStorage(type, data, options)
Unified composable that delegates to the correct handler based on type.
Parameters:
- type: 'query' | 'localStorage' | 'sessionStorage' | InjectionKey - Handler type or injection keydata: MaybeRefOrGetter
- - Reactive reference to syncoptions
- - Handler-specific options (type-checked per handler)
Returns: { data, stop, reset, wasChanged }
- data - The reactive reference passed instop()
- - Unregister and stop syncing (called automatically on unmount)reset()
- - Restore data to its initial statewasChanged: ComputedRef
- - Whether data differs from initial state
Custom handler registration:
- defineContextStorageHandler(name, injectionKey) - Register a custom handlerresolveHandlerInjectionKey(type)
- - Look up an injection key by name
#### useContextStorageQueryHandler
Registers reactive data for URL query synchronization.
Parameters:
- data: MaybeRefOrGetter - Reactive reference to syncoptions?: RegisterQueryHandlerOptions
- prefix?: string
- - Query parameter prefixtransform?: (deserialized, initial) => T
- - Transform functionpreserveEmptyState?: boolean
- - Keep empty state in URLmergeOnlyExistingKeysWithoutTransform?: boolean
- - Only merge existing keys (default: true)
#### ContextStorageQueryHandler
Main handler for URL query synchronization.
Static Methods:
- configure(options): ContextStorageHandlerConstructor - Configure global optionsgetInitialStateResolver(): () => LocationQuery
- - Get initial state resolver
Methods:
- register - Register data for syncsetEnabled(state, initial): void
- - Enable/disable handlersetInitialState(state): void
- - Set initial state
#### useContextStorageLocalStorage
Registers reactive data for localStorage synchronization.
Parameters:
- data: MaybeRefOrGetter - Reactive reference to syncoptions: RegisterWebStorageHandlerBaseOptions
- key: string
- - Storage key (required)prefix?: string
- - Appended to the storage key in bracket notation (e.g. key 'app' + prefix 'filters' = storage key 'app[filters]')transform?: (deserialized, initial) => T
- - Transform functionschema?: ZodSchema
- - Zod schema for validationserializer?: (data: T) => string
- - Custom serializer (default: JSON.stringify)deserializer?: (str: string) => unknown
- - Custom deserializer (default: JSON.parse)
#### useContextStorageSessionStorage
Registers reactive data for sessionStorage synchronization. Same options as useContextStorageLocalStorage.
#### ContextStorageLocalStorageHandler
Handler for localStorage synchronization. Supports cross-tab sync via storage events.
Static Methods:
- configure(options): ContextStorageHandlerConstructor - Configure global optionslistenToStorageEvents?: boolean
- - Enable cross-tab sync (default: true)
#### ContextStorageSessionStorageHandler
Handler for sessionStorage synchronization. Data is scoped to the current tab.
Static Methods:
- configure(options): ContextStorageHandlerConstructor - Configure global optionslistenToStorageEvents?: boolean
- - Listen to storage events (default: false)
####
Scopes a prefix for all descendant useContextStorage calls via provide/inject.
Props:
- name: string | Partial (required) - Prefix to apply. A string applies to all handlers; an object applies per handler type (e.g. { query: 'q', localStorage: 'ls' })
Nested components stack their prefixes using bracket notation. When name changes dynamically, all descendant components are re-created.
All transform helpers support nullable and missable options:
`typescript`
transform.asNumber(value, {
fallback: 0, // Default value
nullable: false, // Allow null return
missable: false, // Allow undefined return
})
Full TypeScript support with type inference:
`typescript`
import type {
ContextStorageHandler,
ContextStorageHandlerConstructor,
IContextStorageQueryHandler,
QueryValue,
SerializeOptions,
} from 'vue-context-storage'
When using Zod schemas, TypeScript will automatically infer types:
`typescript
const FiltersSchema = z.object({
search: z.string().default(''),
page: z.coerce.number().default(1),
})
type Filters = z.infer
// Result: { search: string; page: number }
`
`typescript
import { ref } from 'vue'
import { useContextStorageQueryHandler, transform } from 'vue-context-storage'
const pagination = ref({
page: 1,
perPage: 25,
total: 0,
})
useContextStorageQueryHandler(pagination, {
prefix: 'page',
transform: (data, initial) => ({
page: transform.asNumber(data.page, { fallback: 1 }),
perPage: transform.asNumber(data.perPage, { fallback: 25 }),
total: initial.total, // Don't sync total from URL
}),
})
`
- vue: ^3.0.0vue-router
- : ^4.0.0 || ^5.0.0zod
- : ^4.0.0 (optional - only if using schema validation)
MIT
`bashDevelopment mode (hot reload)
npm run play
$3
`bash
Build library
npm run buildBuild playground for deployment
npm run build:playground
`$3
`bash
Run all checks
npm run checkType checking
npm run ts:checkLinting
npm run lintFormatting
npm run format
``Contributions are welcome! Please feel free to submit a Pull Request.