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-route-state

v1.2.0

Published

Minimal Vue 3 composables for keeping reactive state in Vue Router query parameters.

Readme

vue-route-state

vue-route-state is a small Vue 3 library for storing reactive state in Vue Router query parameters.

The API is intentionally close to useState, but the URL is the source of truth. Values are read from route.query, and writes use router.replace() by default, so browser reload, manual URL edits, back, and forward navigation update the returned refs automatically.

Links

Install

npm install vue-route-state

Peer dependencies:

npm install vue vue-router

Compatibility

The public API is stable as of 1.0.0. Breaking changes are reserved for major versions.

The package targets Vue 3 and Vue Router 4 at runtime. Node.js >=18 is supported for package installation. Use Node.js 24 for repository tooling, builds, tests, and package validation.

Public API

export { useUrlParam, useUrlQueryParam, useUrlState }

useUrlParam

import { useUrlParam } from 'vue-route-state'

const search = useUrlParam('search', {
  type: 'string',
  defaultValue: '',
})

search.value = 'example'

The URL becomes:

?search=example

Deleting a value removes the query parameter:

search.value = null

Pass both route and router when a wrapper or test already has an explicit router context. Use a reactive route source, such as useRoute() or router.currentRoute:

const search = useUrlParam('search', {
  type: 'string',
  defaultValue: '',
  route: router.currentRoute,
  router,
})

useUrlQueryParam

Use useUrlQueryParam when a parameter needs custom parsing or serialization instead of a built-in codec. It is the low-level primitive behind custom URL state wrappers: the library keeps Vue Router wiring, reactivity, query preservation, and navigation mode handling, while your code defines how the raw query value becomes application state.

The parser receives the raw Vue Router query value and defaultValue. The serializer receives the next value and defaultValue. Returning undefined, null, or an empty string removes the query parameter.

Writes use router.replace() by default. Pass history: 'push' to use router.push().

Pass both route and router when a wrapper or test already has an explicit router context. Use a reactive route source, such as useRoute() or router.currentRoute:

const payload = useUrlQueryParam('payload', {
  defaultValue: {},
  parse() {},
  serialize() {},
  route: router.currentRoute,
  router,
})

Legacy URL Values

Use a custom parser when the URL format is fixed by older links or another application, but your component wants a different value shape.

import { useUrlQueryParam } from 'vue-route-state'

const archived = useUrlQueryParam('archived', {
  defaultValue: false,
  parse(value, defaultValue) {
    if (value == null || value === '') {
      return defaultValue
    }

    return value === 'yes'
  },
  serialize(value) {
    return value ? 'yes' : null
  },
})

This reads ?archived=yes as true, treats missing values as false, and removes the parameter when the value is false.

Structured Values

Use a custom serializer when one query parameter represents a small structured value.

const sort = useUrlQueryParam('sort', {
  defaultValue: { key: 'name', order: 'asc' },
  parse(value, defaultValue) {
    const raw = Array.isArray(value) ? value[0] : value
    const [key, order] = String(raw || '').split(':')

    if (!key || !['asc', 'desc'].includes(order)) {
      return defaultValue
    }

    return { key, order }
  },
  serialize(value, defaultValue) {
    if (value.key === defaultValue.key && value.order === defaultValue.order) {
      return null
    }

    return value.key + ':' + value.order
  },
})

This maps ?sort=name:desc to { key: 'name', order: 'desc' }.

Compact JSON Payloads

JSON can be useful for compact internal state, but prefer readable query parameters for public or shareable URLs.

const payload = useUrlQueryParam('payload', {
  defaultValue: {},
  parse(value, defaultValue) {
    const raw = Array.isArray(value) ? value[0] : value

    if (!raw) {
      return defaultValue
    }

    try {
      return JSON.parse(String(raw))
    } catch {
      return defaultValue
    }
  },
  serialize(value, defaultValue) {
    return value === defaultValue ? null : JSON.stringify(value)
  },
})

useUrlState

import { useUrlState } from 'vue-route-state'

const state = useUrlState({
  search: {
    type: 'string',
    defaultValue: '',
  },
  page: {
    type: 'number',
    defaultValue: 1,
    positive: true,
    integer: true,
  },
  enabled: {
    type: 'boolean',
    defaultValue: false,
  },
  periodStart: {
    type: 'date',
    key: 'period_start',
    defaultValue: null,
  },
  tags: {
    type: 'array',
    key: 'tags[]',
    aliases: ['tags'],
    defaultValue: [],
  },
  order: {
    type: 'string',
    defaultValue: 'newest',
    allowedValues: ['newest', 'oldest'],
  },
  sort: {
    type: 'custom',
    defaultValue: { key: 'name', order: 'asc' },
    parse(raw, field) {
      const [key, order] = String(raw || '').split(':')
      return key && order ? { key, order } : field.defaultValue
    },
    serialize(value) {
      return value.key + ':' + value.order
    },
  },
})

state.search.value = 'hello'
state.page.value = 3

Each schema field is returned as a writable computed ref.

Schema

Supported field options:

{
  type,
  key,
  aliases,
  defaultValue,
  allowedValues,
  positive,
  integer,
  omitDefault,
  enabledWhen,
  transform,
  parse,
  serialize,
}

key defaults to the schema field name. omitDefault defaults to true, so assigning the default value removes the parameter from the URL. Set omitDefault: false to write default values explicitly.

Schema field names cannot use returned helper names: patch, clear, reset, snapshot, values, or hasQueryValue.

Use transform(value, field) to normalize values after parsing and before serialization:

search: {
  type: 'string',
  defaultValue: '',
  transform(value) {
    return String(value).trim()
  },
}

Explicit Router Context

useUrlState reads Vue Router from app context by default. Pass both route and router when building wrappers or tests that already have an explicit router context. Use a reactive route source, such as useRoute() or router.currentRoute:

const state = useUrlState(schema, {
  route: router.currentRoute,
  router,
})

Both values are required. Passing only route or only router throws an error. Do not pass a one-time router.currentRoute.value snapshot if the returned refs should react to later navigation.

Conditional fields

enabledWhen and the order option apply to useUrlState().

Use enabledWhen for fields that only apply when other URL state has a particular value:

const state = useUrlState(
  {
    mode: {
      type: 'string',
      defaultValue: 'simple',
      allowedValues: ['simple', 'advanced'],
    },
    detail: {
      type: 'string',
      defaultValue: '',
      enabledWhen: ({ values }) => values.mode === 'advanced',
    },
  },
  {
    order: ['mode', 'detail'],
  },
)

The predicate receives:

{
  field, // current schema field name
  values, // values for the current read or update
  query, // query being read or updated
  route, // current Vue Router route
}

Fields listed in order are resolved first. Remaining fields follow their schema declaration order. This makes dependencies deterministic even when a dependent field is declared before the field it reads. Fields placed before the current field are guaranteed to be normalized before its predicate runs.

When enabledWhen returns false, the field reads as its defaultValue. During the next state write, its primary key and aliases are removed from the URL. patch() can enable a field and assign its value in the same call.

Field groups

Use groups when several fields share the same availability rule:

const state = useUrlState(
  {
    view: {
      type: 'string',
      defaultValue: 'list',
      allowedValues: ['list', 'details'],
    },
    detailTab: {
      type: 'string',
      key: 'detail_tab',
      defaultValue: 'summary',
    },
    detailPage: {
      type: 'number',
      key: 'detail_page',
      defaultValue: 1,
      positive: true,
      integer: true,
    },
  },
  {
    order: ['view', 'detailTab', 'detailPage'],
    groups: {
      details: {
        fields: ['detailTab', 'detailPage'],
        enabledWhen: ({ values }) => values.view === 'details',
      },
    },
  },
)

When a group is disabled, its fields read as their defaultValue. During the next state write, their primary keys and aliases are removed from the URL by default. Set clearWhenDisabled: false to preserve disabled group query keys while still reading default values.

Group predicates receive the same context as field predicates plus group, the group name.

Types

string reads the first query value when Vue Router provides an array. Missing or empty values return defaultValue.

number supports finite JavaScript numbers using Number(value). Invalid values such as abc, NaN, and values less than or equal to zero with positive: true return defaultValue. Set integer: true to reject fractional values such as 2.5. 0 is valid unless positive: true is set.

boolean reads true, false, 1, and 0. It writes canonical values as true and false by default. Set trueValue and falseValue on a boolean field to write custom canonical values, such as 1 and 0. false is a valid value.

date supports YYYY-MM-DD strings and valid Date objects. Invalid dates such as today, 2026-99-99, and 2026-02-31 return defaultValue. Dates are written as YYYY-MM-DD.

custom delegates parsing and serialization to field-level parse(raw, field) and serialize(value, field) functions. Use it when a field needs to store structured values or a URL format that built-in codecs do not cover. Returning undefined, null, or an empty string from serialize removes the query parameter.

array supports arrays of strings. It reads repeated query parameters and comma-separated fallback values, such as ?tags[]=one&tags[]=two and ?tags=one,two. Empty arrays remove the query parameter. Objects in arrays are not supported. With allowedValues, arrays filter invalid items by default. Set invalidValues: 'default' to return defaultValue when any item is unsupported.

patch

Use patch() for related changes. It performs one router navigation and preserves unmanaged query parameters.

await state.patch({
  search: 'hello',
  page: 1,
  enabled: true,
})

patch() updates only provided fields. undefined means “do not change this field”. null removes the field from the URL. Unknown fields throw vue-route-state: Unknown URL state field: name.

The second argument can override the history mode for one action:

await state.patch(
  {
    search: 'hello',
    page: 1,
  },
  {
    history: 'push',
  },
)

Sequential assignments are supported, but they can create separate router navigations:

state.search.value = 'hello'
state.page.value = 2

For connected updates, prefer patch().

clear

await state.clear('search')
await state.clear(['search', 'page'])

This removes only the selected managed parameters. Pass one field name or an array of field names. Without arguments, it removes all parameters managed by the schema:

await state.clear()

Unmanaged query parameters are preserved.

reset

await state.reset()
await state.reset('page')
await state.reset(['page', 'order'])

reset() accepts one field name, an array of field names, or no argument for all fields. It assigns defaultValue for selected fields. Normal serialization rules still apply, including omitDefault.

snapshot and values

const values = state.snapshot()

snapshot() returns a detached plain object. Mutating it does not change URL state.

state.values.value

values is a computed ref containing current parsed values for the whole schema.

hasQueryValue

Use hasQueryValue() when the application needs to distinguish an absent parameter from an explicitly provided value:

state.page.value // 1 for both URLs

// /items
state.hasQueryValue('page') // false

// /items?page=1
state.hasQueryValue('page') // true

The method checks both the primary key and its aliases. It reports whether the query key is present, independently of parsing: an invalid value such as ?page=invalid is present even when state.page.value falls back to its defaultValue.

aliases

Aliases are read-only fallback keys.

tags: {
  type: 'array',
  key: 'tags[]',
  aliases: ['tags'],
  defaultValue: [],
}

Rules:

  1. The primary key has priority.
  2. Aliases are checked only when the primary key is absent.
  3. Writes always use the primary key.
  4. Writes remove stale alias keys.

allowedValues

order: {
  type: 'string',
  defaultValue: 'newest',
  allowedValues: ['newest', 'oldest'],
}

For scalar types, if the URL contains an unsupported value, reading returns defaultValue.

For arrays, invalid values are filtered by default because each item is independent:

tags: {
  type: 'array',
  defaultValue: [],
  allowedValues: ['a', 'b', 'c'],
  invalidValues: 'filter',
}

Use invalidValues: 'default' to make an array return defaultValue when any item is unsupported.

History

Writes use router.replace() by default.

useUrlState(schema, {
  history: 'replace',
})

Use history: 'push' to create browser history entries:

useUrlParam('page', {
  type: 'number',
  defaultValue: 1,
  history: 'push',
})

No-op updates do not call router.replace() or router.push().

Query writes preserve the current route target. Named routes keep their name, params, and hash; unnamed routes keep their resolved path and hash.

patch(), clear(), and reset() can override the configured mode for one navigation:

await state.clear('search', {
  history: 'push',
})

await state.reset('page', {
  history: 'replace',
})

Universal Example

import { computed } from 'vue'
import { useUrlState } from 'vue-route-state'

const state = useUrlState({
  search: {
    type: 'string',
    defaultValue: '',
  },
  page: {
    type: 'number',
    defaultValue: 1,
    positive: true,
  },
  limit: {
    type: 'number',
    defaultValue: 20,
    positive: true,
  },
  sort: {
    type: 'string',
    defaultValue: 'newest',
    allowedValues: ['newest', 'oldest', 'name'],
  },
  tags: {
    type: 'array',
    key: 'tags[]',
    aliases: ['tags'],
    defaultValue: [],
  },
  enabled: {
    type: 'boolean',
    defaultValue: false,
  },
})

await state.patch({
  search: 'example',
  page: 1,
  sort: 'name',
})

const requestParams = computed(() => ({
  search: state.search.value,
  page: state.page.value,
  limit: state.limit.value,
  sort: state.sort.value,
  tags: state.tags.value,
  enabled: state.enabled.value,
}))

Building API requests remains the application's responsibility.

Architecture

The library is split into small modules:

  • codecs/ parse and serialize supported field types.
  • core/create-field.js creates writable computed refs and reads current values.
  • core/update-query.js rebuilds managed query state, preserves unmanaged keys, and performs no-op detection.
  • helpers/ contains schema normalization, query helpers, equality checks, and Vue Router context validation.
  • composables/ exposes useUrlParam and useUrlState.

Development

Use Node.js 24 for local development. The repository tooling follows the packageManager value declared in package.json.

The repository uses a single root eslint.config.js for library source, tests, examples, and playground files.

ESLint checks:

  • no console.log;
  • no unused variables;
  • import order, duplicate imports, and imports before executable code;
  • Vue recommended rules for .vue files.

Prettier handles formatting through the root .prettierrc.

The repository also contains the docs and playground app in playground/. It is built as the public demo site for GitHub Pages and groups examples by scenario: search, pagination, filters, legacy URLs, and custom codecs. Pushes to main deploy it through GitHub Actions.

The app imports the local library source through a Vite alias, so it reflects changes in src/ immediately.

Commands

npm install
npm run lint
npm run lint:fix
npm run format
npm run format:check
npm run test
npm run build
npm run docs:dev
npm run docs:build
npm run docs:preview
npm run package:check