Skip to content
Release notesPinia

Pinia Colada 1.0: the data-fetching layer for Pinia is stable

@pinia/colada 1.0.0 was published on 6 March, after two years of 0.x releases. One breaking change, a codemod to fix it, and an API you can now build on.

vuejs.school team10 Mar 20267 min read@pinia/colada 1.0 · Pinia 3.0
cartitemstotaladd()refsRef<items>Ref<total>

Pinia Colada is the data-fetching layer built on Pinia: queries that cache server data, mutations that change it, and a cache you can inspect like any store. Version 1.0.0 went up on npm on 6 March. The release notes are short because most of the work happened in the 0.x releases before it: one breaking change, two bug fixes in a new compatibility plugin, and a statement of where the library stands.

If your app is on 0.21, there is one change to make: useQuery() and useQueryState() no longer accept two arguments. A codemod rewrites most of the calls in .ts files, and a type check finds the rest, including the ones in components. If you were waiting for a stable version before trying Pinia Colada, the queries, mutations and shared queries below are written the 1.0 way.

What 1.0 promises and what is still 0.x

The release notes describe Pinia Colada as a light, Vue-native solution for async state, with queries "at just <2.5kb". They point out that it is the only library with an official integration for Vue Router's data loaders, and that it has been integrated into code generators such as Hey API and oRPC.

A 1.0 under semantic versioning means the next breaking change waits for a 2.0. You no longer need to read the changelog on every minor bump for renamed options. The package asks for Vue ^3.5.17 and Pinia ^2.2.6 || ^3.0.0, so a Pinia 3 project needs nothing else.

The packages around it stay below 1.0. The Nuxt module, @pinia/colada-nuxt, is at 0.3.2, and @pinia/colada-devtools reached 0.4.5 on the same morning. These official plugins also got releases that day:

  • @pinia/colada-plugin-delay 0.2.0
  • @pinia/colada-plugin-auto-refetch 0.2.6
  • @pinia/colada-plugin-cache-persister 0.1.4
  • @pinia/colada-plugin-tanstack-compat 0.1.0, which is new

A 0.x package may still change its API in any release, so keep exact versions of all these packages in package.json.

The one breaking change

Version 0.21.5, on 22 February, deprecated the two-argument form of useQuery() and useQueryState(), where you passed reusable options and a separate function for their parameters. Version 1.0 removes it:

src/components/ContactList.vueBreaks
const search = ref('')
const { state } = useQuery(contactListQuery, () => ({ search: search.value }))

The single-argument form takes one function that calls the options with their parameters. Vue tracks every reactive value read inside it, so the query follows search exactly as before:

src/components/ContactList.vueWorks
const search = ref('')
const { state } = useQuery(() => contactListQuery({ search: search.value }))

useQueryState(), which reads a cache entry without fetching it, changes the same way and takes the key: useQueryState(() => contactListQuery({ search: search.value }).key). The package ships an ast-grep rule that rewrites most of these calls in .ts files; the upgrade section has the command.

Queries, mutations and defineQuery in 1.0

The docs recommend keeping query options and their keys together in files under src/queries/, written with defineQueryOptions(). A key factory, a small object of functions that build keys, keeps every key for one kind of data consistent:

src/queries/todos.ts
import { defineQueryOptions } from '@pinia/colada'
import { fetchTodo, fetchTodos, type TodoFilter } from '../api/todos'

export const todoKeys = {
  lists: ['todos', 'list'] as const,
  list: (filter: TodoFilter) => ['todos', 'list', { filter }] as const,
  detail: (id: number) => ['todos', 'detail', id] as const,
}

export const todoListQuery = defineQueryOptions((filter: TodoFilter) => ({
  key: todoKeys.list(filter),
  query: () => fetchTodos(filter),
}))

export const todoDetailQuery = defineQueryOptions((id: number) => ({
  key: todoKeys.detail(id),
  query: () => fetchTodo(id),
}))

src/api/todos.ts holds plain functions that call the API and throw on a non-OK response; Pinia Colada decides between success and error by whether the function throws. A component reads the list through the single-function form and adds todos with a mutation that invalidates every list:

src/components/TodoList.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import { createTodo, type TodoFilter } from '../api/todos'
import { todoKeys, todoListQuery } from '../queries/todos'

const filter = ref<TodoFilter>('all')
const { state } = useQuery(() => todoListQuery(filter.value))

const queryCache = useQueryCache()
const { mutate: addTodo, asyncStatus: addStatus } = useMutation({
  mutation: (title: string) => createTodo(title),
  onSettled: () => queryCache.invalidateQueries({ key: todoKeys.lists }),
})
</script>

<template>
  <p v-if="state.status === 'pending'">Loading…</p>
  <p v-else-if="state.status === 'error'">{{ state.error.message }}</p>
  <ul v-else>
    <li v-for="todo in state.data" :key="todo.id">{{ todo.title }}</li>
  </ul>
  <button type="button" :disabled="addStatus === 'loading'" @click="addTodo('New todo')">
    Add todo
  </button>
</template>

Keys are hierarchical, so invalidating ['todos', 'list'] marks the all and open lists stale and refetches whichever one is on screen, while the detail entries are left alone. That is the rule for any mutation: invalidate a key broad enough to cover everything it changed, and no broader. A new todo can appear in any list, but it changes no existing detail.

onSettled runs after success and after failure, which is where the docs put invalidation. Return the promise, as this arrow function does, and the mutation stays in its loading state until the list has come back.

When two components mounted at the same time need the same query and the same state around it, wrap it in defineQuery(). The setup function runs once for the whole app, like a store, so the filter ref is shared:

src/queries/shared-todo-list.ts
import { ref } from 'vue'
import { defineQuery, useQuery } from '@pinia/colada'
import type { TodoFilter } from '../api/todos'
import { todoListQuery } from './todos'

export const useSharedTodoList = defineQuery(() => {
  const filter = ref<TodoFilter>('all')
  const query = useQuery(() => todoListQuery(filter.value))
  return { ...query, filter }
})

A toolbar that changes filter now changes the list in the main view. For parameters that belong to one component, such as an id from props, stay with defineQueryOptions() and a plain useQuery().

Plugins and devtools

A plugin is a function that receives the query cache, the Pinia instance and an effect scope when Pinia Colada is installed. It subscribes to cache actions with Pinia's $onAction(), the same mechanism a store plugin uses.

Since 0.21.4 plugins can watch mutations too. The mutation cache is not one of the plugin's arguments, so apps without mutations can drop it from the bundle; a plugin opts in with useMutationCache(pinia). This one logs every failed query and mutation:

src/plugins/error-log.ts
import { useMutationCache, type PiniaColadaPlugin } from '@pinia/colada'

export function ErrorLogPlugin(): PiniaColadaPlugin {
  return ({ queryCache, pinia }) => {
    queryCache.$onAction(({ name, args, onError }) => {
      if (name !== 'fetch') return
      const [entry] = args
      onError(error => console.error('Query failed', entry.key, error))
    })

    useMutationCache(pinia).$onAction(({ name, onError }) => {
      if (name === 'mutate') onError(error => console.error('Mutation failed', error))
    })
  }
}

Register it with app.use(PiniaColada, { plugins: [ErrorLogPlugin()] }). The docs warn that an aborted or outdated fetch can still reject, so a real logger should filter those out. Retries, a loading delay, auto refetch and cache persistence are all official plugins built this way.

The devtools have been a separate component since 0.21, which removed the older Vue Devtools plugin. Install @pinia/colada-devtools as a dev dependency and put <PiniaColadaDevtools /> at the end of your root component's template. It is stripped from production builds; <PiniaColadaProdDevtools /> keeps it there if you need to inspect a deployed app.

Server rendering and Nuxt

In Nuxt, add the Pinia module and the Pinia Colada module, both with the CLI:

npm install @pinia/colada
npx nuxi module add pinia
npx nuxi module add @pinia/colada-nuxt

Plugin options go in a colada.options.ts file at the project root, exported with satisfies PiniaColadaOptions. The module serialises the query cache into the page payload for you.

The Nuxt guide stresses one difference from useFetch: useQuery() needs no await for server rendering. It registers itself with onServerPrefetch, so the server waits for the data before it renders the page. Add await refresh() after the useQuery() call only if client-side navigation should also wait for the data.

The docs add three cautions:

  • Inside defineQuery(), import useRoute from vue-router rather than using Nuxt's auto-import, or the query runs too often when you navigate to or from the page.
  • defineQuery() is not a store: state it returns next to the query, like the shared filter ref above, is not serialised into the page. Give it the same value on the server and in the browser, or move it into a store.
  • Custom error classes need payload reducers and revivers to survive the trip to the client.

Pinia Colada or TanStack Query

Until 1.0, one reason to choose TanStack Query was stability: its Vue adapter is on version 5 (5.92 is current), while Pinia Colada was still renaming options. That reason is gone. The differences that remain are of shape, and the docs' migration guide lists them:

  • The options are key, query and mutation instead of queryKey, queryFn and mutationFn.
  • The request status is asyncStatus, with the values idle and loading, where TanStack has fetchStatus with fetching.
  • The default staleTime is five seconds instead of zero, so a component that mounts right after another does not refetch.

Pick Pinia Colada when the app already uses Pinia: the cache is a store, it works inside your other stores and navigation guards, and there is one set of devtools to learn. Stay on TanStack Query if a working app already uses it; nothing here is a reason to rewrite. For a migration, the new @pinia/colada-plugin-tanstack-compat adds TanStack-style properties such as isSuccess, isFetching and dataUpdatedAt to Pinia Colada's queries, so components can move over one at a time.

How to upgrade

If you are on 0.21, upgrade now: the single-function form is the only change. Coming from an older 0.x release, first read the breaking changes the changelog lists for each minor version.

Install the new major, commit, then run the codemod and review the diff:

npm install @pinia/colada@1
pnpm --package=@ast-grep/cli dlx ast-grep scan -r node_modules/@pinia/colada/codemods/rules/migration-0-21-to-1-0.yaml -i src

The second command uses pnpm to run ast-grep without installing it; with ast-grep installed globally, ast-grep scan with the same arguments does the same. -i opens an interactive session that shows each rewrite and applies it when you press y, and src is the folder it scans. The changelog warns that the codemod "may not cover all edge cases".

Check it stuck3 questions from the Data fetching with Pinia Colada quiz.
Practice now
Was this helpful?

Keep reading