Using pinia-orm with Vue Query
TanStack Query (Vue Query) handles fetching, caching, retries and background refetching — pinia-orm normalizes the fetched data and gives you relational access to it. They complement each other well: let Vue Query own the request lifecycle and persist every successful response into the store.
Setup
npm install @tanstack/vue-query pinia-orm// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createORM } from 'pinia-orm'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia().use(createORM()))
app.use(VueQueryPlugin)
app.mount('#app')A query-aware base repository
Extend the repository with a useQuery helper that persists the response into the store and returns the hydrated models. The entity name doubles as the query key:
// repositories/QueryRepository.ts
import type { Model } from 'pinia-orm'
import { Repository } from 'pinia-orm'
import type { UseQueryOptions } from '@tanstack/vue-query'
import { useQuery } from '@tanstack/vue-query'
export class QueryRepository<M extends Model = Model> extends Repository<M> {
useQuery (
queryFn: () => Promise<unknown>,
options: Omit<UseQueryOptions, 'queryKey' | 'queryFn'> = {},
) {
return useQuery({
queryKey: [this.getModel().$entity()],
queryFn: async () => {
const data = await queryFn()
// Persist the response — components read the store, not the query data.
this.save(data as any)
return data
},
...options,
})
}
}Usage in a component
<script setup lang="ts">
import { computed } from 'vue'
import { useRepo } from 'pinia-orm'
import { QueryRepository } from '@/repositories/QueryRepository'
import User from '@/models/User'
const userRepo = useRepo(QueryRepository<User>).setModel(User)
const { isLoading, isError, refetch } = userRepo.useQuery(
() => fetch('/api/users').then(response => response.json()),
{ staleTime: 60_000 },
)
// Reactive, normalized and relation-aware — independent of the query cache.
const users = computed(() => userRepo.withAll().get())
</script>
<template>
<p v-if="isLoading">Loading…</p>
<p v-else-if="isError">Failed to load users.</p>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="refetch()">Refresh</button>
</template>If the API response should be the single source of truth — including deletions — persist with
this.fresh(data) instead of this.save(data). See Replacing Whole Data.Table of Contents