vue-qs

Type-safe URL query/search params state for Vue 3 and Nuxt: a useQueryState composable ported from nuqs
6,382 25
Install
npm install vue-qs
Latest Version:0.2.3
Vue:3
License:MIT
Last Updated:Oct 15, 2026
Links: GitHub  ·  npm
Maintainer: iamsomraj

vue-qs

vue-qs social

npm version npm downloads CI docs GitHub Discussions license

Type-safe search params state manager for Vue 3. A port of nuqs that uses Vue idioms: writable refs, v-model, reactive objects and a plugin.

📖 Documentation: https://iamsomraj.github.io/vue-qs/

const page = useQueryState('page', parseAsInteger.withDefault(1));
page.value++; // ?page=2

Why vue-qs?

Filters, search terms, tabs, pagination and sort orders are state your users expect to share, bookmark and restore with the Back button. Storing that state in the URL makes it all work, but parsing and serializing query strings by hand is tedious and error-prone.

vue-qs gives you URL state that feels like regular Vue state: a ref you can bind with v-model, typed by parsers, and kept in sync with the router. It follows nuqs, the popular React library, so its docs, patterns and ideas apply here too.

Features

  • 🧩 The nuqs API: the same parsers, options and behaviour, adapted to Vue
  • 🔒 Type-safe: typed state from parsers, with null removed by .withDefault()
  • ✍️ Writable refs: assign .value or use v-model; .set() for options and promises
  • 📦 Batched updates: changes in the same tick produce a single URL update
  • ⏱️ Throttle & debounce: state is instant, URL writes are rate-limited
  • 🧭 Adapters: Vue Router (and Nuxt), History API, testing, custom
  • 🖥️ Server utilities: createLoader and createSerializer share your parsers
  • 🧪 Testable: an in-memory testing adapter

Installation

npm install vue-qs

Requires Vue 3.3+. vue-router (4.2+ or 5) is an optional peer dependency.

Setup

Install the plugin with an adapter:

import { createApp } from 'vue';
import { createVueQs } from 'vue-qs';
import { createVueRouterAdapter } from 'vue-qs/adapters/vue-router';
// Without a router: import { createHistoryAdapter } from 'vue-qs/adapters/history'

createApp(App)
  .use(router)
  .use(createVueQs({ adapter: createVueRouterAdapter(router) }))
  .mount('#app');

For Nuxt, see the adapters guide.

Usage

<script setup lang="ts">
import { parseAsInteger, useQueryState } from 'vue-qs';

const name = useQueryState('name'); // Ref<string | null>
const count = useQueryState('count', parseAsInteger.withDefault(0)); // Ref<number>
</script>

<template>
  <input v-model="name" placeholder="Your name" />
  <button @click="count++">Count: {{ count }}</button>
  <button @click="count = 0">Reset</button>
  <!-- the default clears ?count -->
</template>

Updating with options

await count.set((c) => c + 1, { history: 'push', scroll: true });
await count.set(null); // removes ?count, count.value === 0

Several keys

import { toRefs } from 'vue';
import { parseAsFloat, useQueryStates } from 'vue-qs';

const coordinates = useQueryStates(
  { lat: parseAsFloat.withDefault(0), lng: parseAsFloat.withDefault(0) },
  { urlKeys: { lat: 'y', lng: 'x' } }
);

coordinates.lat = 48.85; // ?y=48.85
await coordinates.$set({ lat: 1, lng: 2 }); // one URL update
const { lat, lng } = toRefs(coordinates); // refs for v-model

Parsers

parseAsString, parseAsInteger, parseAsFloat, parseAsHex, parseAsIndex, parseAsBoolean, parseAsTimestamp, parseAsIsoDateTime, parseAsIsoDate, parseAsStringEnum, parseAsStringLiteral, parseAsNumberLiteral, parseAsArrayOf, parseAsNativeArrayOf, parseAsJson (with a validator or Standard Schema), and createParser for your own. See the parsers guide.

Options

Option Default Description
history 'replace' 'push' creates a history entry
scroll false Scroll to top after the update
clearOnDefault true Remove the key when the value equals the default
limitUrlUpdates throttle(50) throttle(ms) or debounce(ms)
shallow true Forwarded to custom adapters

See the options guide.

Server

import { createLoader, createSerializer, parseAsInteger, parseAsString } from 'vue-qs/server';

const searchParams = { q: parseAsString.withDefault(''), page: parseAsInteger.withDefault(1) };

const load = createLoader(searchParams);
load(getQuery(event)); // { q: string; page: number }

const serialize = createSerializer(searchParams);
serialize('/search', { q: 'vue', page: 2 }); // '/search?q=vue&page=2'

Testing

import { createVueQs } from 'vue-qs';
import { createTestingAdapter } from 'vue-qs/adapters/testing';

const onUrlUpdate = vi.fn();
mount(Component, {
  global: {
    plugins: [
      createVueQs({ adapter: createTestingAdapter({ searchParams: '?count=1', onUrlUpdate }) }),
    ],
  },
});

Migrating from 0.1.x

0.2 replaces queryRef / queryReactive and codecs with useQueryState / useQueryStates and parsers. See the migration guide.

Community

vue-qs is an open-source project, and feedback shapes where it goes next.

Everyone taking part is expected to follow the Code of Conduct.

If vue-qs is useful to you, a ⭐ on GitHub helps other Vue developers find it.

Credits

vue-qs is a port of nuqs by François Best.

License

MIT © Somraj Mukherjee

Related Packages

vue-route-state

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

0 1
@quantajs/vue

Vue bindings for QuantaJS stores.

347 11
@amirafa/vue3-global-store

SSR-safe global store utility for Vue 3 and Nuxt 3

526

Version History

Version Released Vue License
0.2.3 ^3.3.0 MIT
0.2.2 ^3.3.0 MIT
0.2.1 ^3.3.0 MIT
0.2.0 ^3.3.0 MIT
0.1.18 ^3.3.0 MIT
Pre-releases (7)
Version Released Vue License
0.1.18-beta.7 ^3.3.0 MIT
0.1.18-beta.4 ^3.3.0 MIT
0.1.18-beta.2 ^3.3.0 MIT
0.1.16-beta.1 ^3.3.0 MIT
0.1.15-beta.2 ^3.3.0 MIT