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.