Skip to content
iamsomrajPublic

Repository files navigation

vue-qs

vue-qs: type-safe search params state for Vue and Nuxt, with its question-mark mascot

Mentioned in Awesome Vue npm version npm downloads CI docs GitHub Discussions license

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

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

▶️ Try it: open the playground on StackBlitz

⭐ Listed in Awesome Vue under State Management.

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

vue-qs demo: tabs, search, sort and pagination kept in the URL

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, Nuxt module, 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, add the module, which also auto-imports the composables and parsers:

// nuxt.config.ts
export default defineNuxtConfig({ modules: ['vue-qs/nuxt'] });

See the adapters guide for options.

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 }) }),
    ],
  },
});

vue-qs/testing provides isParserBijective, testParseThenSerialize and testSerializeThenParse to unit-test custom parsers. See the testing guide.

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

Releases

Sponsor this project

Packages

Used by

Contributors

Languages