@volverjs/query-vue is a Vue 3 library that provides a simple way to sync the state of your application with a data source. It is designed to be used with pinia and @volverjs/data repository.
# pnpm
pnpm add @volverjs/query-vue
# yarn
yarn add @volverjs/query-vue
# npm
npm install @volverjs/query-vue --saveFollowing examples are based on a RepositoryHttp instance, but you can use any @volverjs/data repository you want.
First of all, you need to initialize pinia in your application, then you can create a store composable using defineStoreRepository():
import { HttpClient, RepositoryHttp } from '@volverjs/data'
// user-store.ts
import { defineStoreRepository } from '@volverjs/query-vue'
/* Define an User type */
export type User = {
id?: number
username: string
}
/* Create an HttpClient instance */
const httpClient = new HttpClient({
prefixUrl: 'https://my-domain.com'
})
/* Create a RepositoryHttp instance */
const usersRepository = new RepositoryHttp<User>(httpClient, 'users/:id?')
/* Create a store repository composable */
export const useUsersStore = defineStoreRepository(
usersRepository,
// the store name
'users'
)In a component you can use the useUsersStore() composable to get store actions and getters.
defineStoreRepository() accepts an optional third argument to configure the store defaults:
export const useUsersStore = defineStoreRepository(
usersRepository,
'users',
{
/*
* The property (or getter) used to uniquely identify an item.
* Can be a key of the item, a function or an object with a `name`
* and a `get` function (default: 'id').
* For example:
* `keyProperty: 'uuid'`
* `keyProperty: (item) => item.uuid`
* `keyProperty: { name: 'uuid', get: (item) => item.uuid }`
*/
keyProperty: 'id',
/*
* Default cache time (in milliseconds) for every query,
* can be overridden per `read()` with the `persistence` option
* (default: 60 * 60 * 1000)
*/
defaultPersistence: 60 * 60 * 1000,
/*
* Default auto execute debounce (in milliseconds) for every query,
* can be overridden per action with the `autoExecuteDebounce` option
* (default: 0)
*/
defaultDebounce: 0,
/*
* Parameters merged into every request (default: {})
*/
defaultParameters: {},
/*
* The function used to hash the request parameters
* (default: Hash.cyrb53 from `@volverjs/data`)
*/
hashFunction: undefined,
/*
* Interval (in milliseconds) used to schedule the store clean up
* on idle. Pass `0` or `false` to disable the automatic clean up
* (default: 3 * 1000)
*/
cleanUpEvery: 3 * 1000,
}
)read() action allow to read data from the repository:
<script setup lang="ts">
import { useUsersStore } from './user-store'
const { read } = useUsersStore()
/*
* With HttpRepository `read()` execute a
* GET request to https://my-domain.com/users
*/
const { data, isLoading, isError } = read()
</script>
<template>
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else>
<h1>Users</h1>
<ul>
<li v-for="user in data" :key="user.id">
{{ user.username }}
</li>
</ul>
</div>
</template>read() returns an object that contains:
const {
/* Reactive boolean that indicates if the request is loading */
isLoading,
/* Reactive boolean that indicates if the request has failed */
isError,
/* Reactive boolean that indicates if the request has succeeded */
isSuccess,
/* Reactive error object */
error,
/* Reactive array of errors */
errors,
/* Reactive status of the request */
status,
/* Reactive query object */
query,
/* Reactive array of data returned by the repository */
data,
/* Reactive metadata object returned by the repository */
metadata,
/* Reactive first item of the `data` array */
item,
/* Function to execute the `read()` action */
execute,
/* Function to stop `autoExecute` option */
stop,
/* Function to ignore reactive parameters updates */
ignoreUpdates,
/* Function to cleanup the store repository */
cleanup,
/* Function to reset the query */
reset
} = read()The returned object is also awaitable, see Await.
read() accepts an optional parameters map.
const { data } = read({
page: 1
})The parameters map can be reactive, data will be automatically updated on parameters change with autoExecute option:
const parameters = ref({
page: 1
})
const { data } = read(parameters, {
autoExecute: true
})
// ...
parameters.value = {
page: 2
}
// `read()` will be re-executedautoExecute can be stopped with stop() function:
const parameters = ref({
page: 1
})
const { data, stop } = read(parameters, {
autoExecute: true
})
// ...
stop()
// `read()` will not be re-executedA reactive parameters update can be ignored with ignoreUpdates function:
const params = ref({
page: 1
})
const { data, ignoreUpdates } = read(params, {
autoExecute: true
})
// ...
ignoreUpdates(() => {
params.value = {
page: 1,
other: 'value'
}
})You can re-execute the read() action by calling the execute() method with a parameters map that will override the current parameters:
const { data, execute } = read({
sort: 'name',
order: 'asc'
})
// ...
execute({
sort: 'name',
order: 'desc'
})
// `read()` will be re-executed with the given parametersPersistance can be ignored with the force option:
const { data, execute } = read({
sort: 'name',
order: 'asc'
})
// ...
execute(true)
// `read()` will be re-executed ignoring persistance
// ...
execute(
{
sort: 'name',
order: 'desc'
},
true
)
// `read()` will be re-executed with the given parameters ignoring persistanceA read() can be executed later with immediate: false option:
const { data, execute } = read(
{
page: 1
},
{
// `read()` will not be executed
immediate: false
}
)
// ...
execute()
// `read()` will be executed for the first timeA read() can be executed when a condition is met with executeWhen option:
const params = ref({
page: undefined
})
const { data, execute } = read(params, {
executeWhen: computed(() => params.value.page !== undefined)
})
// ...
params.value.page = 1
// `read()` will be executedexecuteWhen can also be function that receives the parameters and returns a boolean:
const params = ref({
page: undefined
})
const { data, execute } = read(params, {
executeWhen: newParams => newParams.page !== undefined
})
// ...
params.value.page = 1
// `read()` will be executedread() accepts the following options:
const {
data
// ...
} = read(
/* The parameters map (default: undefined) */
params,
/* The options object (default: undefined) */
{
/*
* The name of the query (default: undefined)
* if not defined, the query name will be generated
*/
name: undefined,
/*
* Group all queries executed by the same read() action
* and exposes all items in `data` (default: false)
* Can be useful when you need to display a list of items
* (ex. inifinite scroll)
*/
group: false,
/*
* Store query results in a
* separate directory (default: false)
*/
directory: false,
/*
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/*
* Execute the `read()` action immediately (default: true)
*/
immediate: true,
/*
* The query cache time in milliseconds (default: 60 * 60 * 1000)
*/
persistence: 60 * 60 * 1000,
/*
* A boolean reactive parameter (or a function) that indicates
* when the `read()` action should be executed (default: undefined)
* For example:
* `executeWhen: (newParams) => newParams.id !== undefined`
* Or:
* `executeWhen: computed(() => parameters.value.id !== undefined)`
*/
executeWhen: undefined,
/*
* A boolean reactive ref (or a function) that indicates
* when the query should be reset (default: undefined)
* For example:
* `resetWhen: (newParams, oldParams) => newParams.id !== oldParams?.id`
* Or:
* `resetWhen: computed(() => shouldReset.value)`
*/
resetWhen: undefined,
/*
* Automatically execute the `read()` action
* on reactive parameters change (default: false)
*/
autoExecute: false,
/*
* The query auto execute throttle
* in milliseconds (default: 0)
*/
autoExecuteDebounce: 0,
/*
* Automatically execute the `read()` action
* on window focus (default: false)
*/
autoExecuteOnWindowFocus: false,
/*
* Automatically execute the `read()` action
* on document visibility change (default: false)
*/
autoExecuteOnDocumentVisibility: false,
/*
* Additional options passed directly to the repository read method
* (default: undefined)
*/
repositoryOptions: undefined,
}
)submit() action allow to create or update data from the repository. Generally, has the same behavior of read() action but requires a payload parameter.
<script setup lang="ts">
import { useUsersStore } from './user-store'
const { submit } = useUsersStore()
/*
* With HttpRepository `submit()` execute a
* POST request to https://my-domain.com/users
*/
const { isLoading, isError, isSuccess } = submit({
username: 'john.doe'
})
</script>
<template>
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else-if="isSuccess">
Submit success! π
</div>
</template>submit() returns an object that contains:
const {
/* Reactive boolean that indicates if the request is loading */
isLoading,
/* Reactive boolean that indicates if the request has failed */
isError,
/* Reactive boolean that indicates if the request has succeeded */
isSuccess,
/* Reactive error object */
error,
/* Reactive array of errors */
errors,
/* Reactive status of the request */
status,
/* Reactive query object */
query,
/* Reactive array of data returned by the repository */
data,
/* Reactive metadata object returned by the repository */
metadata,
/* Reactive first item of the `data` array */
item,
/* Function to execute the `submit()` action */
execute,
/* Function to stop `autoExecute` option */
stop,
/* Function to ignore reactive parameters updates */
ignoreUpdates,
/* Function to cleanup the store repository */
cleanup
} = submit()The returned object is also awaitable, see Await.
submit() accepts the following options:
const {
isSuccess
// ...
} = submit(
/* The submit payload (required) */
payload,
/* The parameters map (default: undefined) */
params,
/* The options object (default: undefined) */
{
/*
* The name of the query (default: undefined)
* if not defined, the query name will be generated
*/
name: undefined,
/*
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/*
* Execute the `submit()` action immediately (default: true)
*/
immediate: true,
/*
* A boolean reactive parameter (or a function) that indicates
* when the `submit()` action should be executed (default: undefined)
* For example:
* `executeWhen: (newPayload, newParams) => newParams.id !== undefined`
* Or:
* `executeWhen: computed(() => parameters.value.id !== undefined)`
*/
executeWhen: undefined,
/*
* Automatically execute the `submit()` action
* on reactive parameters change (default: false)
*/
autoExecute: false,
/*
* The query auto execute throttle
* in milliseconds (default: 0)
*/
autoExecuteDebounce: 0,
/*
* Automatically execute the `submit()` action
* on window focus (default: false)
*/
autoExecuteOnWindowFocus: false,
/*
* Automatically execute the `submit()` action
* on document visibility change (default: false)
*/
autoExecuteOnDocumentVisibility: false,
/*
* Force the action to be 'create' or 'update' (default: undefined)
* If not defined, the action is inferred from the payload key property
*/
action: undefined,
/*
* Disable the automatic sync of the payload
* with the server response (default: false)
*/
disablePayloadSync: false,
/*
* Additional options passed directly to the repository create/update method
* (default: undefined)
*/
repositoryOptions: undefined,
}
)As read() also submit() can be executed later too with immediate: false option:
<script setup lang="ts">
import type { User } from './user-store'
import { ref } from 'vue'
import { useUsersStore } from './user-store'
const user = ref<User>({
username: ''
})
const { submit } = useUsersStore()
const { isLoading, isError, isSuccess, execute } = submit(user, undefined, {
// `submit()` will not be executed immediately
immediate: false
})
</script>
<template>
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else-if="isSuccess">
New user created! π
</div>
<div v-else>
<h1>Create User</h1>
<form @submit.prevent="execute()">
<input v-model="user.username" type="text" name="username" placeholder="Insert username">
<button type="submit">
Submit
</button>
</form>
</div>
</template>remove() action allow to delete data from the repository:
<script setup lang="ts">
import { useUsersStore } from './user-store'
const { remove } = useUsersStore()
/*
* With HttpRepository `remove()` execute a
* DELETE request to https://my-domain.com/users
*/
const { isLoading, isError, isSuccess } = remove({
// "id" or other "keyProperty" field
id: '123-321'
})
</script>
<template>
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else-if="isSuccess">
Delete success! π
</div>
</template>remove() returns an object that contains:
const {
/* Reactive boolean that indicates if the request is loading */
isLoading,
/* Reactive boolean that indicates if the request has failed */
isError,
/* Reactive boolean that indicates if the request has succeeded */
isSuccess,
/* Reactive error object */
error,
/* Reactive array of errors */
errors,
/* Reactive status of the request */
status,
/* Reactive query object */
query,
/* Function to execute the `remove()` action */
execute,
/* Function to cleanup the store repository */
cleanup,
} = remove({
// ...
})The returned object is also awaitable, see Await.
remove() accepts the following options:
const {
isSuccess
// ...
} = remove(
/* The parameters map (default: undefined) */
params,
/* The options object (default: undefined) */
{
/*
* The name of the query (default: undefined)
* if not defined, the query name will be generated
*/
name: undefined,
/*
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/* Execute the `remove()` action immediately (default: true) */
immediate: true,
/*
* Additional options passed directly to the repository remove method
* (default: undefined)
*/
repositoryOptions: undefined,
}
)The object returned by read(), submit() and remove() is awaitable, like the one returned by VueUse's useFetch. Awaiting it waits for the execution started by the call and resolves to the same plain snapshot that execute() resolves to: booleans and values, not refs.
const { remove } = useUsersStore()
async function deleteUser(id: string) {
const { isSuccess, error } = await remove({ id })
if (isSuccess) {
// the DELETE request succeeded
}
else {
// the DELETE request failed, `error` tells why
console.error(error)
}
}The snapshot contains:
const {
/* Boolean that indicates if the request has succeeded */
isSuccess,
/* Boolean that indicates if the request has failed */
isError,
/* Boolean that indicates if the request has been aborted */
aborted,
/* The first error, if any */
error,
/* The array of errors */
errors,
/* The query object */
query,
/* The metadata object returned by the repository */
metadata,
/* `read()` and `submit()` only: the data array returned by the repository */
data,
/* `read()` and `submit()` only: the first item of the `data` array */
item,
} = await read()Awaiting follows these rules:
- it never rejects: a failed request (also one that cannot be built, for example because of a missing path parameter) resolves with
isError: trueand itserror, an aborted request (for example one replaced by anexecute()with other parameters) resolves withaborted: true. WithRepositoryHttpfrom@volverjs/data2.0.x, aread()that fails without an HTTP response (a network error, an invalid JSON body) is reported as aborted, so do not treatabortedas a success; - it settles on the execution started by the call only: with
autoExecute, the executions started later by a parameters change do not change the result, awaitexecute()to wait for one of them; - when the call starts no execution (
immediate: false, orexecuteWhenfalse when the action is called), it resolves at once to the current snapshot and sends no request, soisSuccessstaysfalseuntil an execution succeeds; - awaiting never starts a request, so
Promise.resolve(),Promise.all()or returning the object from anasyncfunction do not execute the action twice.
The awaited values are plain, they do not update. Keep the returned object to use both the result and the reactive fields:
const users = read()
const { isSuccess } = await users
// `isSuccess` is a boolean, `users.data` is still a reactive arrayBefore 2.1.0 awaiting an action returned the object itself at once. If your code awaits an action and then uses its refs (data.value, or binds them in the template after a top-level await in <script setup>), drop the await or keep the returned object and await it on its own as above.
To execute an action later and wait for it, create it with immediate: false and await execute():
const { execute } = remove(undefined, { immediate: false })
async function deleteUser(id: string) {
const { isSuccess } = await execute({ id })
// ...
}The template refs of the provider components are not awaitable. If you expose the object returned by an action with defineExpose(), leave then out the same way (const { then, ...exposed } = read()), otherwise an async function that returns the component instance resolves to the snapshot instead.
An action is cleaned up when the effect scope that created it is disposed: the component setup() when the component unmounts, but also an effectScope() or a Pinia setup store. An action created with no active scope, for example in an event handler as above, has no owner: when it is awaited, its query is released as soon as the awaited execution settles (unless keepAlive is true), and the next clean up drops it together with the items only that query referenced, so the reactive fields of the returned object go back to their empty values. If you create an action outside a scope and do not await it, call cleanup() when you no longer need it.
@volverjs/query-vue also exposes some useful components:
ReadProvider is a component that allows to use read() action in a component tree:
<script setup lang="ts">
import { useUsersStore } from './user-store'
const { ReadProvider } = useUsersStore()
</script>
<template>
<ReadProvider v-slot="{ isLoading, isError, data }">
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else>
<h1>Users</h1>
<ul>
<li v-for="user in data" :key="user.id">
{{ user.username }}
</li>
</ul>
</div>
</ReadProvider>
</template>ReadProvider exposes all the properties returned by read() in the v-slot scope and accepts the following props:
<template>
<ReadProvider
v-bind="{
/* The parameters map (default: undefined) */
params,
/* The `read()` options object (default: undefined) */
options,
}"
/>
</template>SubmitProvider is a component that allows to use submit() action in a component tree:
<script setup lang="ts">
import type { User } from './user-store'
import { ref } from 'vue'
import { useUsersStore } from './user-store'
const { SubmitProvider } = useUsersStore()
const user = ref<User>({
username: ''
})
</script>
<template>
<SubmitProvider v-slot="{ isLoading, isError, isSuccess, execute }" v-model="user">
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else-if="isSuccess">
New user created! π
</div>
<div v-else>
<h1>Create user</h1>
<form @submit.prevent="execute()">
<input
v-model="user.username"
type="text"
name="username"
placeholder="Insert username"
>
<button type="submit">
Submit
</button>
</form>
</div>
</SubmitProvider>
</template>SubmitProvider exposes all the properties returned by submit() in the v-slot scope and accepts the following props:
<template>
<SubmitProvider
v-bind="{
/* The payload (required) */
modelValue,
/* The parameters map (default: undefined) */
params,
/* The `submit()` options object (default: { immediate: false }) */
options,
}"
/>
</template>The v-model directive provides a two-way binding so the reactive payload object will be updated when the submit() action is executed (for example with the server response).
By default SubmitProvider will not execute the submit() action immediately, but you can change this behavior with immediate option.
RemoveProvider is a component that allows to use remove() action in a component tree:
<script setup lang="ts">
import { useUsersStore } from './user-store'
const { RemoveProvider } = useUsersStore()
</script>
<template>
<RemoveProvider
v-slot="{ isLoading, isError, isSuccess, execute }"
:params="{ id: '123-321' }"
>
<div v-if="isLoading">
Loading...
</div>
<div v-else-if="isError">
An error occurred! π
</div>
<div v-else-if="isSuccess">
Delete success! π
</div>
<div v-else>
<button type="button" @click="execute()">
Delete
</button>
</div>
</RemoveProvider>
</template>RemoveProvider exposes all the properties returned by remove() in the v-slot scope and accepts the following props:
<template>
<RemoveProvider
v-bind="{
/* The parameters map (required) */
params,
/* The `remove()` options object (default: { immediate: false }) */
options,
}"
/>
</template>By default RemoveProvider will not execute the remove() action immediately, but you can change this behavior with immediate option.
This repository ships an installable Claude Code skill that helps agents build Vue 3 data fetching and mutations with @volverjs/query-vue. It covers store scaffolding with defineStoreRepository, the read/submit/remove actions and their options, the provider components, and the caching/normalized-item-cache model.
npx skills add volverjs/query-vueSee skills/volverjs-query-vue/README.md for details.
@volverjs/query-vue is inspired by React Query.