Skip to content

useQueryStates ​

Binds a group of query keys at once. Use it when several params form one state or require atomic batch writers.

ts
import { codecs, useQueryStates } from '@vuqs/core'

const { values, patch, replace, clear } = useQueryStates({
  q: codecs.string.withDefault(''),
  sort: codecs.literal(['asc', 'desc'] as const).withDefault('asc'),
  page: codecs.integer.withDefault(1),
})

The argument is a schema: a map of logical names to codecs. Each map key becomes the query key. Use queryParam for a param whose key differs from its name, a multi-key param, or a builder modifier.

Return value ​

ts
import type { UseQueryStatesReturn } from '@vuqs/core'
import { codecs, defineQuerySchema, useQueryStates } from '@vuqs/core'

const schema = defineQuerySchema({
  q: codecs.string.withDefault(''),
  sort: codecs.literal(['asc', 'desc'] as const).withDefault('asc'),
  page: codecs.integer.withDefault(1),
})

const query = useQueryStates(schema)
type QueryApi = UseQueryStatesReturn<typeof schema>

values: a reactive value map ​

values.q is the value, not a ref. Read it, assign it, v-model it:

vue
<template>
  <input v-model="values.q">
  <select v-model="values.sort"> … </select>
  <button @click="values.page++">Next</button>
</template>

Params declaring a .withDefault() read as T: absence resolves to the default. For the string and number params above:

ts
values.q.trim() // string, no guard needed
values.page + 1 // number

A param without a default reads as T | undefined.

Replace, don't mutate

values tracks assignment, not in-place mutation. To change an array param, assign a new array:

ts
values.tags = [...values.tags, 'new'] // ✅ navigates
values.tags.push('new') // ❌ no navigation

Per-field refs with toQueryRefs ​

The grouped values map has no per-field .set or .clear. To create field refs with those methods, project the composable with toQueryRefs:

ts
import { toQueryRefs } from '@vuqs/core'

const query = useQueryStates(schema)
const { q, page } = toQueryRefs(query)

q.value = 'laptop' // write, like values.q = 'laptop'
page.set(2, { history: 'push' }) // per-call options, back on a field
q.clear() // remove ?q

Each ref writes through the same binding, so it inherits the same clearing rule, including any default a module layers on top. If you only need one param, use useQueryState instead.

Whole-object ref with toQueryRef ​

When the value is the whole state, such as a form model or an API request object, toQueryRef binds the entire schema to a single writable ref. Reading gives a plain snapshot (absent params omitted); assigning replaces the state, clearing any param the assigned object leaves out or sets to undefined:

ts
import { toQueryRef } from '@vuqs/core'

const query = useQueryStates(schema)
const filters = toQueryRef(query)

filters.value = { q: 'laptop', sort: 'asc' } // set q + sort, clear page
filters.value = { q: 'laptop', page: undefined } // set q, clear page + sort
filters.value = { ...filters.value, page: 1 } // keep the object, set page

The snapshot keeps a stable reference while its content is unchanged, so binding it with v-model on the whole object does not loop. Use toQueryRefs (plural) for per-field refs, toQueryRef (singular) for the object as one value.

patch: partial write ​

Updates supplied params in one atomic transaction and preserves omitted params:

  • omit preserves the param.
  • undefined clears the state, reverting to its resolved default.
  • a value sets it.
ts
patch({ q: 'laptop', page: 1 }) // set q and page, leave sort alone
patch({ sort: undefined }) // clear sort
patch({ q: 'phone' }, { history: 'push' }) // with per-call options

A supplied object replaces that param's representation instead of merging into it. With record.a=7&record.b=20 in the URL, patch({ record: { b: 10 } }) leaves only record.b=10; the omitted child falls back to its default on read. Omitting record from the patch keeps the whole representation. Partial objects are accepted for composed params, and undefined clears them:

ts
patch({ record: { b: 10 } }) // record.a=7 is dropped, a reads its default
patch({ record: undefined }) // clear the whole record

A module can add or remove params while the write is prepared. The write callbacks run once over the complete request, a param they introduce joins the same transaction, and a param they remove is cleared rather than left as it was. A param the request and the callbacks never touch is preserved. The same rules apply to values assignments, toQueryRefs and single-param setters.

replace: whole-state write ​

Sets the given params and clears every param absent or explicitly undefined, in one atomic transaction. Use it when the argument is the complete state, such as applying a saved view:

ts
replace({ q: 'laptop', sort: 'desc' }) // q + sort set, page cleared
replace({ q: 'laptop', page: undefined }) // q set, page + sort cleared

clear: reset everything ​

ts
clear() // every param back to its default, one navigation (replace({}))
clear({ history: 'push' }) // with options

Reads after composing modules ​

Modules can change what a param reads, for example by making it absent in some context. use returns the composable with the reads that follow those effects, so capture the end of the chain and read through it:

ts
const query = useQueryStates(schema)
  .use(withContext({ active, only: { page: ['products'] } }))

query.values.page // number | undefined, even though `page` has a static default

A reference kept before a .use() call observes the same binding at runtime but keeps the earlier types. Refs from toQueryRefs(query) follow the same read types as values, and the write types stay the params' explicit values.

Coalescing: many writes, one navigation ​

Assigning several values.* in a row creates separate one-param transactions. Calling patch with multiple keys creates one multi-param transaction. Both produce exactly one history entry when they occur in the same coalescing window:

ts
function resetFilters() {
  values.q = ''
  values.sort = 'asc'
  values.page = 1
} // → a single navigation, not three

Several useQueryState calls using the same adapter coalesce too. Coalescing only combines navigation; it does not merge those calls into one transaction. Prefer useQueryStates when a write is one logical operation: patch, replace, and clear apply their complete key set atomically before observers run.

Composing modules ​

useQueryStates returns a composable with a .use(module) method. Each call runs the module, merges its API onto the composable, and widens the return type:

ts
import { withRuntimeDefaults } from '@vuqs/core/modules'

const { values, setDefaults } = useQueryStates(schema)
  .use(withRuntimeDefaults())

Grouped state example ​

vue
<script setup lang="ts">
import { codecs, useQueryStates } from '@vuqs/core'
import { computed } from 'vue'

const { values, patch, clear } = useQueryStates({
  q: codecs.string.withDefault(''),
  sort: codecs.literal(['asc', 'desc'] as const).withDefault('asc'),
  page: codecs.integer.withDefault(1),
})

const results = computed(() => runSearch(values.q, values.sort, values.page))

function search(term: string) {
  // A new search resets to page 1, in one navigation.
  patch({ q: term, page: 1 })
}
</script>

<template>
  <input :value="values.q" @input="search(($event.target as HTMLInputElement).value)">
  <select v-model="values.sort">
    <option value="asc">Price ↑</option>
    <option value="desc">Price ↓</option>
  </select>

  <ul>
    <li v-for="r in results" :key="r.id">{{ r.name }}</li>
  </ul>

  <button @click="values.page++">Next page</button>
  <button @click="clear()">Reset</button>
</template>

useQueryState vs useQueryStates ​

useQueryStateuseQueryStates
Bindsone keya group
Returnsa QueryStateRef (.value, .set, .clear){ values, patch, replace, clear }
Per-param options✅ on .set / .clearvia patch / replace (whole batch)
Multi-param coalescing✅ across calls in the same window✅
Atomic batch API—✅ patch / replace / clear
Compose a module✅ .use()✅ .use()
A ref to pass around✅toQueryRefs(query)

Released under the MIT License. Inspired by nuqs.