---
title: "TanStack Query or Pinia Colada: caching your server data · vuejs.school"
description: "Build the same todo list with TanStack Query and with Pinia Colada, see what a query cache does for you, and get a clear rule for which one to install."
canonical_url: "https://vuejs.school/community/blog/tanstack-query-or-pinia-colada"
meta:
  "og:description": "Build the same todo list with TanStack Query and with Pinia Colada, see what a query cache does for you, and get a clear rule for which one to install."
  "og:image": "https://vuejs.school/_og/d/a_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGEgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGE%2Cdescription_~QnVpbGQgdGhlIHNhbWUgdG9kbyBsaXN0IHdpdGggVGFuU3RhY2sgUXVlcnkgYW5kIHdpdGggUGluaWEgQ29sYWRhLCBzZWUgd2hhdCBhIHF1ZXJ5IGNhY2hlIGRvZXMgZm9yIHlvdSwgYW5kIGdldCBhIGNsZWFyIHJ1bGUgZm9yIHdoaWNoIG9uZSB0byBpbnN0YWxsLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90YW5zdGFjay1xdWVyeS1vci1waW5pYS1jb2xhZGEi%2Cs_m5vhwdb4b9rYH5Nj.png"
  "og:image:alt": "TanStack Query or Pinia Colada: caching your server data · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "TanStack Query or Pinia Colada: caching your server data · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGEgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGE%2Cdescription_~QnVpbGQgdGhlIHNhbWUgdG9kbyBsaXN0IHdpdGggVGFuU3RhY2sgUXVlcnkgYW5kIHdpdGggUGluaWEgQ29sYWRhLCBzZWUgd2hhdCBhIHF1ZXJ5IGNhY2hlIGRvZXMgZm9yIHlvdSwgYW5kIGdldCBhIGNsZWFyIHJ1bGUgZm9yIHdoaWNoIG9uZSB0byBpbnN0YWxsLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90YW5zdGFjay1xdWVyeS1vci1waW5pYS1jb2xhZGEi%2Cs_m5vhwdb4b9rYH5Nj.png"
  "twitter:image:alt": "TanStack Query or Pinia Colada: caching your server data · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What a query library does

A query library is a cache for server data, keyed by what the data is. You give it a key such as `['todos']` and a function that fetches, and it does four jobs you would otherwise write by hand.

- It caches. The second component that asks for `['todos']` gets the data that is already there, with no request.
- It deduplicates. Two components mounting at the same moment produce one request, not two.
- It tracks freshness. Data becomes stale after a time you choose. Stale data is refetched in the background when a component mounts or the window regains focus, and the old data stays on screen while that happens.
- It reports status. Pending, error and success are flags you read, not booleans you flip.

The last piece is writes. Both libraries call a write a mutation, and both give you a way to say "this list is out of date, fetch it again" once a mutation finishes. That step is called invalidation, and it is the part hand-written code most often forgets.

## Put the fetch calls in one module

Both versions of the list import these two functions, so the components never call `fetch` themselves.

*src/api/todos.ts*

```
export interface Todo {
  id: number
  title: string
  done: boolean
}

export async function fetchTodos(): Promise<Todo[]> {
  const res = await fetch('/api/todos')
  if (!res.ok) throw new Error(`Loading todos failed with ${res.status}`)
  return res.json()
}

export async function createTodo(title: string): Promise<Todo> {
  const res = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title }),
  })
  if (!res.ok) throw new Error(`Creating a todo failed with ${res.status}`)
  return res.json()
}
```

Throwing on a non-OK response matters. `fetch` only rejects on a network failure, and both libraries decide between success and error by whether your function throws.

## Build the list with TanStack Query

`@tanstack/vue-query` is the Vue adapter of TanStack Query, the library many developers know from React. Version 5 has been the current major since October 2023, and the Vue and React adapters share one core package, `@tanstack/query-core`.

```
npm install @tanstack/vue-query
```

Register the plugin once. It creates the query client, the object that owns the cache.

*src/main.ts*

```
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'

createApp(App).use(VueQueryPlugin).mount('#app')
```

The component reads with `useQuery` and writes with `useMutation`.

*src/components/TodoList.vue*

```
<script setup lang="ts">
import { ref } from 'vue'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { createTodo, fetchTodos } from '../api/todos'

const queryClient = useQueryClient()

const { data: todos, isPending, isError, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 30_000,
})

const title = ref('')
const { mutate: addTodo, isPending: isSaving } = useMutation({
  mutationFn: createTodo,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

function submit() {
  if (!title.value.trim()) return
  addTodo(title.value)
  title.value = ''
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="title" aria-label="New todo" />
    <button type="submit" :disabled="isSaving">Add</button>
  </form>
  <p v-if="isPending">Loading todos</p>
  <p v-else-if="isError">{{ error?.message }}</p>
  <ul v-else>
    <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
  </ul>
</template>
```

`queryKey` is an array, and it is the identity of the data: any component that uses `['todos']` shares this cache entry. `data` is a ref that starts as `undefined`, `isPending` is true until the first result arrives, and `isError` with `error` cover the failure case. A failed request is [retried three times by default](https://tanstack.com/query/v5/docs/framework/vue/guides/important-defaults), with growing waits, so the error message appears about seven seconds after the first failure.

`staleTime: 30_000` says the list counts as fresh for thirty seconds. The default is zero, which means every new mount refetches in the background while showing what it has.

The mutation's `onSuccess` calls `invalidateQueries` with the same key. Invalidation marks the cached list stale and refetches it straight away wherever it is on screen. Keys match by prefix, so invalidating `['todos']` also refreshes `['todos', { page: 2 }]`. Pass `exact: true` when you do not want that.

## Build the list with Pinia Colada

Pinia Colada is a data fetching layer built on top of Pinia, by the author of Pinia. The cache is itself a Pinia store, so it appears in Vue DevTools next to your own stores. The library is small: its README puts the baseline at about 2 kB.

```
npm install pinia @pinia/colada
```

Its `package.json` asks for Vue 3.5.17 or newer and Pinia 2.2.6 or newer, Pinia 3 included. Then register both plugins in `src/main.ts`.

*src/main.ts*

```
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.use(PiniaColada)
app.mount('#app')
```

The order matters: Pinia first, then Pinia Colada. Installed the other way round, Pinia Colada throws "root pinia plugin not detected" in development.

The component's script changes, and its template keeps every line but one.

*src/components/TodoList.vue*

```
<script setup lang="ts">
import { ref } from 'vue'
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import { createTodo, fetchTodos } from '../api/todos'

const queryCache = useQueryCache()

const { data: todos, isPending, error } = useQuery({
  key: ['todos'],
  query: fetchTodos,
  staleTime: 30_000,
})

const title = ref('')
const { mutate: addTodo, isLoading: isSaving } = useMutation({
  mutation: createTodo,
  onSettled: () => queryCache.invalidateQueries({ key: ['todos'] }),
})

function submit() {
  if (!title.value.trim()) return
  addTodo(title.value)
  title.value = ''
}
</script>
```

Compared with the TanStack version, four things change:

- **Shorter names.** `key` and `query` instead of `queryKey` and `queryFn`, `mutation` instead of `mutationFn`, and `useQueryCache()` instead of `useQueryClient()`.
- **No `isError`.** In the template, change `v-else-if="isError"` to `v-else-if="error"`. If you keep the old line, Vue warns that `isError` is not defined, and a failed load shows an empty list instead of the message.
- **Two statuses.** `status` is `'pending'`, `'success'` or `'error'` and describes the data, while `asyncStatus` is `'idle'` or `'loading'` and describes the request. So `isPending` means "no data yet" and `isLoading` means "a request is in flight": a background refetch of stale data shows `isLoading` true with `isPending` false. A mutation exposes `isLoading` the same way.
- **Different defaults.** `staleTime` defaults to five seconds, where TanStack's default is zero. A failed request is not retried, so the error shows at once; retries come from a separate plugin, `@pinia/colada-plugin-retry`.

This version also invalidates in `onSettled` rather than `onSuccess` (both libraries have both hooks). `onSettled` runs after success or failure, so the list is refreshed even when the server rejected the write. The [migration guide from TanStack Vue Query](https://pinia-colada.esm.dev/cookbook/migration-tvq.html) lists more differences.

Keys can be functions. `key: () => ['todos', route.params.id as string]`, with `route` from Vue Router's `useRoute()`, gives each id its own cache entry and refetches when the param changes. The `as string` is for TypeScript: a key holds only JSON values, and create-vue's strict settings type the param as possibly `undefined`.

To ask for fresh data from code, Pinia Colada offers `refresh()`, which deduplicates and respects `staleTime`, and `refetch()`, which always hits the network.

## Fix a list that does not update after a write

Add a header component that shows a count with `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })`. Then write the mutation like this.

*src/components/TodoList.vue**Breaks***

```
const { mutate: addTodo } = useMutation({
  mutationFn: createTodo,
})
```

The request succeeds. The server has the new todo. The list in this component and the count in the header both keep showing the cached array, because nothing told the cache that `['todos']` changed. Reaching into `todos.value` and pushing the new item does not fix it: `data` is read-only in TanStack Query, and in both libraries the next refetch would overwrite your edit anyway.

*src/components/TodoList.vue**Works***

```
const queryClient = useQueryClient()

const { mutate: addTodo } = useMutation({
  mutationFn: createTodo,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
```

One line, and every component that reads `['todos']` updates, including ones you have not written yet. In Pinia Colada the same line is `onSettled: () => queryCache.invalidateQueries({ key: ['todos'] })`. After every write, invalidate the key you changed.

## Which one to install

Install TanStack Query if you are shipping to users this quarter and the team wants an API that will look the same next year. Version 5 is two years old, the documentation is thorough, there is a devtools panel in `@tanstack/vue-query-devtools`, and almost any question has already been answered somewhere. The cost is a second mental model next to Pinia, with its own client and its own devtools.

Install Pinia Colada if your app already lives in Pinia and you are willing to follow a moving API. Everything sits in one place: the query cache is a store, `useQueryCache()` works inside your other stores, and the whole thing is smaller to learn. Expect to touch option names between now and a stable release.

**Check it stuck**3 questions from the Query libraries, TanStack Query and Pinia Colada quiz.

[**Practice now**](https://vuejs.school/learn/data/query-libraries-tanstack-and-pinia-colada?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Deep dive · 6 min read**Streaming AI responses: fetch, ReadableStream and the AI SDK**](https://vuejs.school/community/blog/streaming-ai-responses-fetch-readablestream-and-the-ai-sdk) [Tutorial · 8 min read**Authentication basics: tokens, cookies and where to keep them**](https://vuejs.school/community/blog/authentication-basics-tokens-cookies-and-where-to-keep-them)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-13","description":"Build the same todo list with TanStack Query and with Pinia Colada, see what a query cache does for you, and get a clear rule for which one to install.","name":"TanStack Query or Pinia Colada: caching your server data","url":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada","breadcrumb":{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/tanstack-query-or-pinia-colada"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-13","description":"Build the same todo list with TanStack Query and with Pinia Colada, see what a query cache does for you, and get a clear rule for which one to install.","headline":"TanStack Query or Pinia Colada: caching your server data","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGEgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGE%2Cdescription_~QnVpbGQgdGhlIHNhbWUgdG9kbyBsaXN0IHdpdGggVGFuU3RhY2sgUXVlcnkgYW5kIHdpdGggUGluaWEgQ29sYWRhLCBzZWUgd2hhdCBhIHF1ZXJ5IGNhY2hlIGRvZXMgZm9yIHlvdSwgYW5kIGdldCBhIGNsZWFyIHJ1bGUgZm9yIHdoaWNoIG9uZSB0byBpbnN0YWxsLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90YW5zdGFjay1xdWVyeS1vci1waW5pYS1jb2xhZGEi%2Cs_m5vhwdb4b9rYH5Nj.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/tanstack-query-or-pinia-colada#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"TanStack Query or Pinia Colada: caching your server data","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGEgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGE%2Cdescription_~QnVpbGQgdGhlIHNhbWUgdG9kbyBsaXN0IHdpdGggVGFuU3RhY2sgUXVlcnkgYW5kIHdpdGggUGluaWEgQ29sYWRhLCBzZWUgd2hhdCBhIHF1ZXJ5IGNhY2hlIGRvZXMgZm9yIHlvdSwgYW5kIGdldCBhIGNsZWFyIHJ1bGUgZm9yIHdoaWNoIG9uZSB0byBpbnN0YWxsLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90YW5zdGFjay1xdWVyeS1vci1waW5pYS1jb2xhZGEi%2Cs_m5vhwdb4b9rYH5Nj.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGEgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VGFuU3RhY2sgUXVlcnkgb3IgUGluaWEgQ29sYWRhOiBjYWNoaW5nIHlvdXIgc2VydmVyIGRhdGE%2Cdescription_~QnVpbGQgdGhlIHNhbWUgdG9kbyBsaXN0IHdpdGggVGFuU3RhY2sgUXVlcnkgYW5kIHdpdGggUGluaWEgQ29sYWRhLCBzZWUgd2hhdCBhIHF1ZXJ5IGNhY2hlIGRvZXMgZm9yIHlvdSwgYW5kIGdldCBhIGNsZWFyIHJ1bGUgZm9yIHdoaWNoIG9uZSB0byBpbnN0YWxsLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90YW5zdGFjay1xdWVyeS1vci1waW5pYS1jb2xhZGEi%2Cs_m5vhwdb4b9rYH5Nj.png"}]}
```