npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-router-query-sync

v2.0.0

Published

Reactive, typed view of Vue Router query params with codecs and defaults

Readme

vue-router-query-sync

Draft docs for 2.0.0-alpha.0. API only — no tests, migration guide or final docs yet.

A reactive, typed view of the Vue Router query string. The URL (route.query) is the single source of truth: reads parse route.query, writes navigate the router. The library holds no state of its own and never throws because of URL contents.

  • Vue 3 + Vue Router 4 (peer dependencies).
  • Codecs + defaults per field; defaults never appear in the URL.
  • Arrays use native repeated keys (?a=1&a=2).
  • Batched navigation: several writes in one tick collapse into one push/replace.
  • SSR-safe: router is read via useRouter()/useRoute(), no global/module state.

Install

npm install vue-router-query-sync

useQueryState(schema, options?)

Returns { state, patch, reset }. state is a reactive object of schema fields only (no methods on it); access fields without .value. The type is inferred from the schema.

import { useQueryState, field } from 'vue-router-query-sync'

enum SortType { Popular = 'popular', New = 'new' }

const { state, patch, reset } = useQueryState({
  sort:   field.enum(SortType, { default: SortType.Popular, history: 'push' }),
  brands: field.stringArray(),        // default: []
  page:   field.number({ default: 1 }),
  from:   field.custom(dateCodec),    // default: null
})

state.sort = SortType.New             // write -> router navigation
state.brands                          // read  -> parses route.query (or pending write)
patch({ brands: ['lg'], page: 2 })    // several fields, ONE navigation
reset()                               // all fields back to defaults (keys removed)
  • Read: pending write (same tick) → else route.query[key]deserialize → on undefined/throw → default. Reading a field right after writing it returns the new value.
  • Write: serialize → if null or equal to serialize(default) → the key is removed.
  • Query keys not described in the schema are left untouched.
  • Destructuring state loses reactivity — use Vue's toRefs(state).

Single parameter

There is no separate single-param helper: use a one-key schema and toRef.

import { toRef } from 'vue'
import { useQueryState, field } from 'vue-router-query-sync'

const { state } = useQueryState({ tab: field.enum(['all', 'favorite'] as const) })
const tab = toRef(state, 'tab') // WritableComputedRef<'all' | 'favorite' | null>
tab.value = 'favorite'

prefix — namespacing keys

When the same schema is used more than once on a page (e.g. one paginated component per tab), pass a prefix so each instance owns its own URL keys and they don't collide. Field names in state stay unprefixed; only the URL key becomes ${prefix}-${key}.

const users  = useQueryState({ page: field.number({ default: 1 }) }, { prefix: 'users' })
const orders = useQueryState({ page: field.number({ default: 1 }) }, { prefix: 'orders' })

users.state.page = 3   // URL: ?users-page=3
orders.state.page = 5  // URL: ?users-page=3&orders-page=5

users.state.page       // reads users-page only — independent from orders

With a unique prefix per instance, keys never collide. Each instance keeps its own state in the URL, so switching between tabs and back restores their pagination; a direct link reproduces everything.

The prefix applies to the whole schema. To namespace only some keys, split by scope: keep shared keys in a plain (unprefixed) call and private keys in a prefixed one.

const { state: shared } = useQueryState({ sort: field.enum(SortType) })            // ?sort=…
const { state }         = useQueryState({ page: field.number({ default: 1 }) },
                                        { prefix: 'users' })                        // ?users-page=…

If you'd rather keep the URL minimal, render inactive tabs with v-if and call reset() in onUnmounted to drop the leaving tab's keys.

field

field.* builds a schema field: its type, default, history, and codec. Options are shared across all types: { default?: T; history?: 'push' | 'replace' } (history defaults to 'replace'). Passing default narrows the field type from T | null to T.

| Builder | Field type | Notes | | --- | --- | --- | | field.string(options?) | string \| null | | | field.number(options?) | number \| null | | | field.boolean(options?) | boolean \| null | URL is 'true' / 'false' | | field.stringArray(options?) | string[] | repeated keys; default always [] | | field.enum(input, options?) | member of input | see below | | field.custom(codec, options?) | T from codec | default null if unset |

field.enum accepts either a readonly string array or a TS enum object (string or numeric). Numeric enums have their reverse mapping filtered out and URL strings are coerced to numbers. A value outside the set becomes undefineddefault (behaviour mirrors z.enum).

Codecs

A codec is the low-level serialize/deserialize pair a field is built on. Use field.custom with defineCodec for types beyond the built-ins.

import { defineCodec, type QueryCodec } from 'vue-router-query-sync'

interface QueryCodec<T> {
  serialize(value: T): string | string[] | null    // null = remove key
  deserialize(raw: string | string[]): T | undefined // undefined = use default
}

const dateCodec = defineCodec<Date | null>({
  serialize: (d) => (d ? d.toISOString().slice(0, 10) : null),
  deserialize: (raw) => {
    const t = Date.parse(Array.isArray(raw) ? raw[0] : raw)
    return Number.isNaN(t) ? undefined : new Date(t)
  },
})

defineCodec is an identity helper for type inference. Every built-in field.* type is backed by a codec internally.

Batching & history

All writes in the same tick — field assignments, patch, reset — accumulate into one pending batch (kept per router instance) and are applied as a single navigation via queueMicrotask. The batch is layered over the last scheduled state, so reads within the same tick see pending writes. If any changed field in the batch has history: 'push', the whole batch uses router.push; otherwise router.replace. Query keys keep insertion order — existing keys stay in place, newly added keys are appended (e.g. ?tab=users&page=2).

Exports

import {
  useQueryState,
  field,
  defineCodec,
  type QueryCodec,
  type FieldOptions,
  type Field,
  type QuerySchema,
  type QueryValues,
  type UseQueryStateOptions,
  type UseQueryStateReturn,
} from 'vue-router-query-sync'

Playground

npm run dev

A catalog page exercises every field type plus reset/patch, and a second route shows the single-parameter pattern (toRef over a one-key schema). The current route.query is rendered on screen.

License

MIT © Ivan Chikachev