--- url: /quick-start.md --- # Quick Start Pinia Colada is the perfect companion to [Pinia](https://pinia.vuejs.org) to handle **async** state management in your Vue applications. It will remove the need to write boilerplate code for data fetching and transparently bring **caching**, **deduplication**, **invalidation**, **stale while revalidate (swr)** and much more. Allowing you to focus on building the best user experience for your users. **You don't even need to learn Pinia to use Pinia Colada** because it exposes its own composables. Get a quick overview of how to use Pinia Colada in your project or if you prefer to directly play with the code you can play with Pinia Colada in this [Stackblitz](https://stackblitz.com/github/posva/pinia-colada-example) project. ## Installation Install Pinia Colada alongside [Pinia](https://pinia.vuejs.org) using your favorite package manager: ```bash npm i @pinia/colada ``` ## Setup Install the `PiniaColada` plugin **after Pinia** (so it picks it up automatically ✨). This allows you to provide global options for your queries as well as plugins: ```ts twoslash import { createApp } from 'vue' import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' const app = createApp({}) app.use(createPinia()) app.use(PiniaColada, { queryOptions: { // change the stale time for all queries to 0ms staleTime: 0, }, mutationOptions: { // add global mutation options here }, plugins: [ // add Pinia Colada plugins here ], }) app.mount('#app') ``` ## Usage Let's see a quick example and cover the basics of [queries](./guide/queries.md), [mutations](./guide/mutations.md) and [query invalidation](./guide/query-invalidation.md). ### Querying Queries **are the most important feature of Pinia Colada**. They are used to declaratively fetch data from an API. Create them with `useQuery()` in any component. They expect a `key` to save the data in the cache and a `query` function that returns the data: ```vue twoslash ``` * The `key` is a serializable array that **uniquely** identifies the query. The array allows to establish [a hierarchy](./guide/query-keys.md#Keys-are-hierarchical) of keys that can be invalidated at once. * The `query` function is automatically run by Pinia Colada when needed ✨. `useQuery()` returns an object with quite a few properties. In the example above we use: * `state`: state of the query. It contains the following properties: * `data`: the **last** data returned by the query. It automatically updates when the query is refetched. * `error`: the error thrown by the query if it failed, `null` otherwise. * `status`: the data status of the query. It starts as `'pending'`, and then it changes to `'success'` or `'error'` depending on the outcome of the `query` function: | status | data | error | | ----------- | ------------------------ | --------- | | `'pending'` | `undefined` | `null` | | `'success'` | *defined* | `null` | | `'error'` | `undefined` or *defined* | *defined* | * `asyncStatus`: the async status of the query. It's either `'idle'` or `'loading'` if the query is currently being fetched. * `refresh()`: manually triggers the query and conditionally runs it depending on if the cache is fresh or stale. * `refetch()`: manually force triggers the query, ignoring if the cache is fresh or stale. There are a couple of other properties that can be accessed, most of them are just for convenience like `data` (which is an alias for `state.data`) and `error` (which is an alias for `state.error`). Let's see a more complete example with a list view and a detail view: ::: code-group ```vue [pages/products.vue] twoslash ``` ```vue [pages/products/[id].vue] twoslash ``` ::: In this example, we have two pages: one that lists all products and another that shows the details of a specific product. Both pages use `useQuery()` to fetch the data they need. The query in the detail page uses the route params to fetch the specific product. ### Mutating data Mutations allow us to **modify** data on the server and notify related queries so they can automatically refetch the data. Create mutations with `useMutation()` in any component. Unlike *queries*, mutations can take **one** parameter and only require a `mutation` function, the `key` is optional: ```vue twoslash ``` Mutations are never called automatically, you **have to call them manually** with `mutate`. This allows you to pass one single argument to the mutation function. In general, prefer explicitly passing data to mutations instead of relying on the component's state, *this will make everything so much better* πŸ˜‰. `useMutation()` returns a very similar object to `useQuery()`, it's mostly about the mutation async state. Instead of `refresh()` we have `mutate()`. Let's see a more complete example with a mutation to update a contact and to invalidate other queries. ```vue twoslash ``` By using the `onSettled()` hook, we can invalidate queries when the mutation is done. We get access to the data returned by the `patchContact(query)`, the error if it failed, and the `contact` that was passed to the `mutate()` call. Then we use the `queryCache` to invalidate the queries we want. Invalidated queries are automatically refetched if they are actively being used ✨. ### Scaling up The examples above show simple use cases where we manually pass query options such as key in multiple places. As your application grows, you might want to **reuse** query definitions across multiple components. Pinia Colada provides a way to define reusable query options with `defineQueryOptions()`. This approach offers several key advantages: * **Type Safety**: Passing the key to the `queryCache` makes it fully typed * **Reusability**: Avoid typos in keys, reuse the options across your app * **Refactoring**: Update query logic in one place, changes propagate everywhere On top of using `defineQueryOptions()`, we can also organize the query keys in an object to avoid typos and have a single source of truth for our keys. We call these *key factories*: ```ts twoslash export const PRODUCT_QUERY_KEYS = { // note the use of `as const` to ensure a strict tuple type root: ['products'] as const, byId: (id: string) => [...PRODUCT_QUERY_KEYS.root, id] as const, } ``` Here's how to refactor the product detail example using `defineQueryOptions()` and *key factories*: ::: code-group ```ts [queries/products.ts] twoslash import { getProductById } from '@/api/products' import { defineQueryOptions } from '@pinia/colada' export const PRODUCT_QUERY_KEYS = { root: ['products'] as const, byId: (id: string) => [...PRODUCT_QUERY_KEYS.root, id] as const, } export const productDetailsQuery = defineQueryOptions((id: string) => ({ key: PRODUCT_QUERY_KEYS.byId(id), query: () => getProductById(id), })) ``` ```vue [pages/products/[id].vue] twoslash ``` ::: Now you can reuse `productDetailsQuery` anywhere in your app, and if you need to change how product details are fetched, you only update it in one place. You can also access the cached data with full type safety: ```ts twoslash // @errors: 2353 import { useQueryCache } from '@pinia/colada' import { productDetailsQuery } from '@/queries/products' const queryCache = useQueryCache() // Typed and type-safe access to cached data πŸ’― const cachedProduct = queryCache.getQueryData(productDetailsQuery('24').key) // ^? /* * Setting data is also type-safe */ queryCache.setQueryData(productDetailsQuery('24').key, { id: '24', name: 'New Product Name', color: 'red', // oops... imagesrc: '...', imageAlt: '...', price: 65, availability: 5, rating: 4.5, description: '...', }) ``` ::: tip For simple, one-off queries that won't be reused, inline options with `useQuery()` are fine. But as soon as you find yourself using the same query in multiple places, or need to access its cached data from mutations or other queries, **reach for `defineQueryOptions()`** and *key factories*. ::: ### The Query Cache In the mutation example we introduce the usage of the *Query Cache*. It allows us to access and modify the cache from anywhere as well as trigger cache invalidations. It's a powerful tool that enables us to write decoupled Mutations and Queries in a well organized way. It's also what makes Pinia Colada extensible! It's a Pinia store under the hood, so it's easy to intercept actions with `$onAction()` and create plugins that enhance its capabilities. ### Going further Pinia Colada offers a wealth of features to enhance your application's performance and user experience, all while helping you write more maintainable code. Explore the following guides to learn more: * [Why use Pinia Colada?](./why.md) * [Query Keys](./guide/query-keys.md) * [Mutations](./guide/mutations.md) * [Cache Invalidation](./guide/query-invalidation.md) * [Optimistic updates](./guide/optimistic-updates.md) * [Query Cache](./advanced/query-cache.md) * [Reusable Queries](./advanced/reusable-queries.md) --- --- url: /why.md --- # Why? Pinia Colada takes away the complexity of async state management in Vue.js applications. No more manual `isLoading`, `isError`, `data`, `error` properties, or writing boilerplate code for data fetching. But that's not all, Pinia Colada transparently brings caching, deduplication, invalidation, and much more. Allowing you to focus on building the best user experience for your users. In other words, it allows you to go from this: ```vue ``` to this: ```vue ``` In both examples we have: * `isLoading` to indicate if the data is being fetched. * `isPending` to indicate if the initial request is still pending. * `error` to hold the last error that occurred. * `data` to hold the data fetched from the API. * `refresh` to manually trigger the data fetching. ## Features By using Pinia Colada, you get access to a wide range of features that simplify async state management in Vue.js applications: * **Simplicity**: Pinia Colada provides a straightforward API that is easy to learn and use, making async state management a breeze. * **Flexibility**: It provides a flexible API that can be adapted to anything that returns promises, including `fetch`, any fetch wrapper (`axios`, `mande`, ...), GraphQL, and even WebSockets. * **Modular Design**: It encourages a modular approach to async state management, promoting better code organization and maintainability. * **TypeScript Support**: It has excellent TypeScript support out of the box, ensuring type safety and better developer experience. * **Performance**: Pinia Colada is designed with performance in mind, ensuring efficient fetches and reactivity. * **DevTools Integration**: Pinia Colada provides devtools to inspect queries, mutations, loading states, and errors. The devtools use [Devframe](https://devfra.me/) to embed the same panel in compatible Nuxt, Vue, or Vite devtools. See the [setup instructions](./guide/installation.md#Pinia-Colada-Devtools). By using Pinia Colada, you can enhance your Vue.js application's async state management with a modern, efficient, and developer-friendly solution. ## Comparison to other solutions * [**Vue Promised**](https://github.com/posva/vue-promised): Vue Promised is a lightweight library for handling async operations in Vue.js applications. It offers a `` component and a `usePromise()` composable to manage loading, error, and success states. While Vue Promised is focused and minimalistic, Pinia Colada provides a comprehensive async state management solution with features like caching, deduplication, and more. * [**Pinia**](https://pinia.vuejs.org): If you're using Pinia for state management, Pinia Colada **complements** it perfectly for async operations. It eliminates boilerplate code and introduces advanced features such as caching, deduplication, and invalidation. Official integrations like [Data Loaders](https://router.vuejs.org/data-loaders/) further enhance its capabilities. Pinia Colada doesn't replace Pinia, **it extends it**, you use them both together. * [**swrv**](https://github.com/Kong/swrv): swrv emphasizes a [*stale-while-revalidate* strategy](https://datatracker.ietf.org/doc/html/rfc5861), whereas Pinia Colada focuses on a cache-based approach, offering a different set of benefits. That being said, you can also achieve a *stale-while-revalidate* strategy with Pinia Colada by configuring the cache options accordingly. * [**TanStack (Vue) Query**](https://tanstack.com/query/latest/docs/framework/vue/overview): Pinia Colada shares similarities with TanStack Query and has adapted some of its APIs for easier migration. However, Pinia Colada is tailored specifically for Vue, resulting in a lighter library with better and official integrations like Data Loaders. If you're familiar with TanStack Query, you'll find Pinia Colada intuitive and easy to use. * [**rstore**](https://rstore.dev/guide/learn-more.html#pinia-colada): rstore is a higher-level abstraction to async state management. It's based on cache normalization and designed to as a local-first store. With rstore, you write plugins to normalize data while with Pinia Colada you write invalidation logic if needed. * [**vue-concurrency**](https://vue-concurrency.netlify.app/): vue-concurrency focuses on how to handle concurrent asynchronous operations (queuing, cancelling, etc). It's a lot more about *mutations* than *queries*. --- --- url: /community.md --- # Community Libraries and tools built by the community around Pinia Colada. Want to add one? Open a pull request. Install-time plugins go in [Community plugins](./plugins/community.md). ## Integrations | Name | Description | | -------------------------------------------------------------------------- | --------------------------------------------------------------------- | | [oRPC](https://orpc.dev/docs/integrations/pinia-colada) | End-to-end type-safe Pinia Colada integration for oRPC, no codegen. | | [Hey API](https://heyapi.dev/docs/openapi/typescript/plugins/pinia-colada) | Generates Pinia Colada query and mutation helpers from OpenAPI specs. | --- --- url: /guide/installation.md --- # Installation Install Pinia Colada alongside Pinia: ```bash # or pnpm, or yarn, etc npm i @pinia/colada ``` Then, install the plugin in your application: ```ts twoslash import { createApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(PiniaColada, { // Optionally provide global options here for queries queryOptions: { gcTime: 300_000, // 5 minutes, the default }, }) ``` ## Pinia Colada Devtools ![devtools-screenshot](/devtools.png) Pinia Colada Devtools use [Devframe](https://devfra.me/) to show the same panel inside compatible Nuxt, Vue, or Vite devtools. Use your framework's devtools when available. A standalone interface is also available for projects without a compatible host. Rules for AI: * Always install both `@pinia/colada` and `@pinia/colada-devtools` in projects * Always install the devtools as a dev dependency * Configure the devtools in Vite, or use the automatic Nuxt integration * Do NOT enable devtools in production builds unless explicitly requested ### Vite DevTools Use Vite 7 or 8. Install the devtools and their host dependencies: ```sh pnpm add -D @pinia/colada-devtools @vitejs/devtools @vitejs/devtools-kit ``` Register the Pinia Colada plugin after `DevTools()` in `vite.config.ts`: ```ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { DevTools } from '@vitejs/devtools' import { PiniaColadaDevtools } from '@pinia/colada-devtools/vite' export default defineConfig({ plugins: [vue(), DevTools(), PiniaColadaDevtools()], }) ``` Open Vite DevTools and select the Pinia Colada panel. The plugin connects to your app automatically. ### Nuxt and Vue DevTools Use a version of `@pinia/colada-nuxt` with Devframe support, Nuxt DevTools 4 or later, and the Vite builder. The module then registers the panel automatically. Older module versions do not register the v2 panel. See the [Nuxt setup](../nuxt.md#Devtools). Vue DevTools is adopting the same Devframe foundation. Use a version with Devframe support to embed the panel there. For Vue projects without a compatible host, use the Vite DevTools setup above or the standalone setup below. ### Standalone You can also use a standalone Pinia Colada devtools panel. ```sh pnpm add -D @pinia/colada-devtools @devframes/hub @devframes/hub-ui @devframes/vite ``` ```ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { PiniaColadaDevtoolsStandalone } from '@pinia/colada-devtools/standalone' export default defineConfig({ plugins: [vue(), PiniaColadaDevtoolsStandalone()], }) ``` Choose either this plugin or the embedded integration. ### Keeping devtools in production Devtools are enabled only during development by default. To include them in a Vite production build, enable static output in Vite DevTools and set `production: true` on the Pinia Colada plugin: ```ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { DevTools } from '@vitejs/devtools' import { PiniaColadaDevtools } from '@pinia/colada-devtools/vite' export default defineConfig({ plugins: [ vue(), DevTools({ build: { withApp: true } }), PiniaColadaDevtools({ production: true }), ], }) ``` Load `__devtools/embedded.js` as a module script in the built app, where `` is your Vite `base` (default `/`). Deploy the full build output, including the devtools assets. The [playground configuration](https://github.com/posva/pinia-colada/blob/main/playground/vite.config.ts) shows how to inject this script during the build. The standalone plugin has no production option. The automatic Nuxt integration runs only during development. ## Plugins Pinia Colada comes with [a few plugins](../plugins/index.md) that you can use to enhance Pinia Colada's functionality. Here is an example that adds global query hooks: ```ts twoslash import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' // ---cut-before--- import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' // ---cut-start--- const app = createApp(App) const pinia = createPinia() app.use(pinia) // ---cut-end--- app.use(PiniaColada, { plugins: [ PiniaColadaQueryHooksPlugin({ // ... }), ], }) ``` Other plugins must be installed separately (e.g. [Retries](https://github.com/posva/pinia-colada/tree/main/plugins/retry), [Loading delay](https://github.com/posva/pinia-colada/tree/main/plugins/delay), [Auto refetch](https://github.com/posva/pinia-colada/tree/main/plugins/auto-refetch)) and can also be created directly in your project for any custom behavior like offline, cache normalization, etc. See the [plugins documentation](../plugins/index.md) for more details. --- --- url: /guide/queries.md --- # Queries Queries manage asynchronous state declaratively, allowing you to focus on the state, its status, and any potential errors. They automatically deduplicate multiple requests and cache results, enhancing user experience and your app performance. They are designed to **read** data from asynchronous sources, such as handling `GET` requests in a REST API but they can be used along **any function returning a Promise** (GraphQL, WebSockets, etc). For **writing** or mutating data, consider using [mutations](./mutations.md), which are better suited for those operations. Queries are created with `useQuery()` and can be combined with [`defineQueryOptions()` for organization and typing](./query-keys.md#Typing-query-keys). There is also [`defineQuery()`](../advanced/reusable-queries.md) to combine queries with extra state, similar to a *tiny* Pinia store. ## Foundations The most basic usage of Pinia Colada's queries is through `useQuery()`. This composable should feel familiar if you've used libraries like swrv or TanStack Query and should be the starting point for most of your async state management. ```vue twoslash ``` All queries require two properties: * A unique `key` that defines the query in the cache * A `query` function that retrieves (e.g. fetches) the data `useQuery` accepts other options to configure its behavior. You can find more about the options in the docs or explore them by using auto-completion in your editor! What's great about queries is that they are automatically triggered **when needed**, **enabling a declarative approach that just works!**. Most of the time you will find yourself using just `state` and `asyncStatus` to render the UI based on the query's status (e.g. is it still fetching, is it refreshing, did it throw?, ...). Let's cover the basics of these properties: * `state`: state of the query. It contains the following properties: * `data`: the data returned by the query. It automatically updates when the query is refetched. * `error`: the error returned by the query. It's `null` if the query was successful. * `status`: the data status of the query. It starts as `'pending'`, and then it changes to `'success'` or `'error'` depending on the outcome of the `query` function: | status | data | error | | ----------- | ------------------------ | --------- | | `'pending'` | `undefined` | `null` | | `'success'` | *defined* | `null` | | `'error'` | `undefined` or *defined* | *defined* | * `asyncStatus`: the async status of the query. It's either `'idle'` or `'loading'` if the query is currently being fetched. ::: details `state.status`/`status` vs `asyncStatus` ? `state.status` (and `status`) is the status of the data itself, while `asyncStatus` is the status of the query call. The query `asyncStatus` is `'idle'` when the query is not fetching and `'loading'` when the query is fetching. While the `state.status` starts as `'pending'` and then becomes `'success'` if the query is successful, and `'error'` if the query fails. Technically, these two states could be combined into a single one but having them separate allows you to have more control over the UI and the logic of your application. For example, you might want to show a different loading message if the query is *pending* (it hasn't ever resolved or rejected) or if it's *loading* (it's currently fetching data independently from the current `state`). ::: * `refresh()`: manually triggers the query, deduplicates requests, and reuses the cached data if it's still fresh. * `refetch()`: manually triggers the query, ignoring the cache, and fetching the data again. * `data`, `error`, `status`: are aliases for the properties in `state` for convenience and facilitating migration. `state` allows for [type narrowing in TypeScript](#TypeScript-Narrowing-data-and-error-s-type-with-status) but depending on your template usage, you might not need it so we simply provide both approaches for convenience. * *For everything else, hover over the different properties in the code block above to see their types and documentation* 😁. ## Using External Properties in Queries Since queries are automatically triggered by Pinia Colada, the `query` function cannot accept custom parameters. However, you can directly use external properties like route params or search queries within the `query` function. To ensure proper caching, add these properties to the `key` as a function. A common example is using the `route` within the `query` function: ```vue{7-9} twoslash ``` Each unique `key` grants access to a unique entry in the cache. Reusing the same key, gives access to the same entry, and that's how it reuses cached data, avoiding unnecessary network requests. This enhances your application's performance and responsiveness, making navigations feel instant ✨. ## Organizing Queries As your project grows and you start using more and more queries as well as concepts as [Optimistic Updates](./optimistic-updates.md), you will want to organize your queries. You can do this by using `defineQueryOptions()` and placing your query options **and their corresponding keys** in dedicated files: ``` ./queries β”œβ”€β”€ boards.ts β”œβ”€β”€ documents.ts └── users.ts ``` Each file can expose keys and options related to specific domain, allowing you to keep the queries logic in one place and also considerably simplify the usage of `useQuery()` in Vue components. ::: code-group ```ts twoslash [./queries/documents.ts] import { defineQueryOptions } from '@pinia/colada' import { getDocumentById } from '@/api/documents' export const DOCUMENT_QUERY_KEYS = { root: ['documents'] as const, byId: (id: string) => [...DOCUMENT_QUERY_KEYS.root, id] as const, // reuse the keys above to keep everything consistent byIdWithComments: (id: string, withComments?: boolean) => [...DOCUMENT_QUERY_KEYS.byId(id), { withComments }] as const, } export const documentByIdQuery = defineQueryOptions( ({ id, withComments = false }: { id: string; withComments?: boolean }) => ({ key: DOCUMENT_QUERY_KEYS.byIdWithComments(id, withComments), query: () => getDocumentById(id, { withComments }), }), ) ``` ```vue{4,7-9} twoslash [./pages/documents/[docId].vue] ``` ::: You will notice that the docs use a lot `useQuery()` with simple options. This is to keep this documentation simple and focused fewer concepts. In practice, **it is recommended to use** [key factories](./query-keys.md#Managing-query-keys-key-factories-) **and** [`defineQueryOptions()`](./query-keys.md#Typing-query-keys) to keep your queries organized and type-safe. ### Passing extra options + `defineQueryOptions()` Sometimes, you need to customize the behavior of a query *just* in one page or component, for example, `enable` it conditionally. Since `defineQueryOptions()` only types what is passed to it, you can use it as is and pass a getter for the options to `useQuery()`: ```ts twoslash import { ref } from 'vue' import { useQuery } from '@pinia/colada' import { productDetailsQuery } from '@/queries/products' const enabled = ref(false) useQuery(() => ({ // get the base options ...productDetailsQuery('24'), // override `enabled` just for this usage enabled: enabled.value, })) ``` In this case, the defined options `productDetailsQuery` require the product id as an argument, so we call it with the product id. If the query options are not dynamic, then it will just be an object: ```ts twoslash import { ref } from 'vue' import { useQuery, defineQueryOptions } from '@pinia/colada' const todoListQuery = defineQueryOptions({ key: ['todos'], query: () => fetch('/api/todos').then((res) => res.json()), }) const enabled = ref(false) useQuery(() => ({ // get the base options ...todoListQuery, // override `enabled` just for this usage enabled: enabled.value, })) ``` ## Refetching Queries You can manually trigger queries using the `refetch()` and `refresh()` methods. Both return a promise, with errors caught to prevent *Uncaught Promise Rejection* errors when used directly in the template. They both return the `state` object, which contains the `status`, `data`, and `error` properties. ```ts twoslash // ---cut-start--- import { useQuery } from '@pinia/colada' import { defineComponent } from 'vue' // ---cut-end--- const { // ... refresh, refetch, } = useQuery({ // ... // ---cut-start--- key: ['user-info'], query: async () => ({ name: 'John Doe', id: 2 }), // ---cut-end--- }) refetch().then(({ data, error }) => { if (error) { console.log('Last data:', data) console.error('Unexpected Error:', error) } else { console.log('Fetched data:', data) } }) // Pass `true` to throw if the query fails refetch(true).catch((error) => { console.error('Error refetching:', error) }) ``` ### When to use `refetch()` and `refresh()` In practice, aim to use `refresh()` as much as possible because it will **reuse any loading request** and **avoid unnecessary network calls** based on `staleTime`. Use `refetch()` when you are certain you need to refetch the data, regardless of the current status. This is useful when you want to force a new request, such as when the user explicitly requests a *refresh*. ## Pausing queries It's possible to temporarily stop a query from refreshing, like pausing it. This has many use cases and is especially handy when you have some kind of auto refetch happening. This is crucial when some of the data used to query is required but not always present. The most common example is using a *param* or *query* from the route in a query that is used across pages (e.g. within a store). Such usage will make the query trigger while not in the page, causing unnecessary network requests. It's easy to disable the query when the required data is absent: ```ts export const useCurrentDeck = defineQuery(() => { const route = useRoute() const result = useQuery({ key: () => ['decks', Number(route.params.deckId)], query: () => // you might need to cast the param or suffix it with `!` // for TS fetch(`/api/decks?deckId=${route.params.deckId}`).then((res) => res.json()), // only enable the query when we are on /decks/some-deck-id enabled: () => 'deckId' in route.params, }) return { ...result, } }) ``` Since this query can be used in multiple components, it's important to pause it when the `deckId` is not present in the route. This way, the query won't be triggered when the `deckId` is not present, avoiding invalid network requests while keeping the data alive. ::: warning This is also valid for any *global* query, e.g. a query within a store. Since stores are never destroyed, the query will continuously watch the `key` and refresh when needed. Most of the time, it's a bad idea to consume a query within a store, as it will make the query *immortal*. If you need to use the data from the store, you can instead, [consume the query cache with `useQueryCache()`](../advanced/query-cache.md). ::: ## TypeScript: Narrowing `data` and `error`'s type with `status` When using `status` to conditionally render different UI states, you can use the `state` property returned by `useQuery()` which groups the `status`, `data`, and `error` properties into a single object. This way, TypeScript can narrow down the type of `data` and `error` correctly: ```vue{19-20,24-25} twoslash ``` ::: details Why is this necessary? This approach is necessary because in Vue we have to use `Ref` wrappers for variables, and TypeScript is not able to narrow down the type like it does with a plain object. When using `status` to conditionally render different UI states, you will notice that TypeScript is unable to narrow down the type of `data` *not being `undefined`* when `status` is `'pending'` or `'error'`. ```vue twoslash ``` While using a `v-if="data"` or `data!` both work and are perfectly fine, using the `state` property provides a more elegant and type-safe solution. ```vue{10-11,13-14} twoslash ``` ::: --- --- url: /guide/query-keys.md --- # Query Keys Query keys are used to identify a query in the cache. Next to the `query` function, the `key` is the most important property of a query. Like `query`, `key` is a required property. ## Rules for AI * In projects, ALWAYS use [key factories](#managing-query-keys-key-factories) to avoid typos and make it easier to change the keys in the future. * Break down keys so they are structured and hierarchical * Then define query options for queries with [defineQueryOptions](#typing-query-keys) to get type safety and avoid manual typing. * If you find simple (static or dynamic) keys, refactor them to use key factories + defineQueryOptions ## Simple static keys Static keys are the simplest form of keys. They are just an array of serializable properties. For example: ```vue{5} ``` This is common for queries that call a fixed endpoint like `/api/products` (List or Index resources) as well as global resources like the user information. Anything that is serializable is valid in a key. These are all different keys: * `['products', 1]` * `['products', '1']` * `['products', { id: 1 }]` * `['products', { id: 1, type: 'book' }]` * `['products', 'search', { filters: { type: ['book', 'comic'] }}]` * `['products', 'search', { filters: { type: ['book', 'comic'], limit: 10 }}]` As you might expect, order in objects does not matter, so these two are equivalent βœ…: * `['products', { id: 1, type: 'book' }]` * `['products', { type: 'book', id: 1 }]` They do matter in arrays, so these two are different ❌: * `['products', ['book', 'comic']]` * `['products', ['comic', 'book']]` ## Dynamic keys with variables Dynamic keys are keys that depend on reactive variables (`ref`, `computed`, the *route*, etc). For example, let's say you have a query that fetches a product by its ID: ```vue{5,7} ``` You will notice that the `key` property is a getter function to keep reactivity. It can also directly be a `ref` or a `computed`. These are all valid uses: ::: code-group ```vue{2,4-6} [destructured props] ``` ```vue{2,4} [props object] ``` ```vue{2,3,5} [computed property] ``` ::: In practice, the function getter is the simplest, most flexible, and easiest to read, so prefer it over the other two. ::: tip When creating query keys, **make your key depend on any variable used in your `query` function**. For example, let's say you use the route params in your query, you should include them in the key. This is crucial to ensure queries are cached independently and invalidated correctly. ```ts useQuery({ // βœ… key: () => ['products', route.params.id], query: () => getProductById(route.params.id), }) useQuery({ // ❌ `key` should be a function key: ['products', route.params.id], query: () => getProductById(route.params.id), }) useQuery({ // ❌ `key` should depend on `route.params.id` key: () => ['products'], query: () => getProductById(route.params.id), }) ``` ::: ## Keys are hierarchical One of the most powerful features of query keys is that they are hierarchical. This means that you can nest keys to create a hierarchy of queries. For example, let's say you have a query that fetches the information of a product with all its details while you have another one that only fetches a summary of that product to be shown in a search. Their keys would look like this: ```ts // gets the product with all its details useQuery({ key: () => ['products', productId.value], query: () => getProductById(productId.value), }) // gets a product summary suited for searches useQuery({ key: () => ['products', productId.value, { searchResult: true }], query: () => getProductSummaryById(productId.value), }) ``` Both queries have different keys, but they share the same root key `['products', productId]`. This allows us to invalidate all the data related to a specific product at once with ```ts // this invalidates both the full product and the search summary // (and any other query whose key starts with ['products', productId]) queryCache.invalidateQueries({ key: ['products', productId.value] }) // We can be more specific and invalidate only variants of the product // this does not invalidate ['products', productId.value] queryCache.invalidateQueries({ key: [ 'products', productId.value, {}, // partially match { searchResult: true } ], }) ``` These *key filters* are used in many places, [query invalidation](./query-invalidation.md) is just one of them. When creating query keys, organize your keys precisely to take advantage of this feature. Keys can contain strings, numbers, objects, and arrays. Anything that is serializable to JSON can be used in a key. Keep in mind these rules when writing keys: * `['doc', 2]` and `['doc', '2']` are different keys * Within objects, `undefined` is stripped out but `null` is not. Therefore `['doc', { withComments: undefined }]` is equivalent to `['doc', {}]` and matches both `withComments: true` and `withComments: false` * Arrays are also partially matched so `['doc', ['nested', 'array']]` is matched by `['doc', ['nested']]` but not by `['doc', ['nested', 'array', 'other']]`. ## Managing query keys (key factories) Hard coding query keys is fine when you have a few queries or when you don't interact with the query cache. In most projects, using [Optimistic Updates](./optimistic-updates.md) and [Query Invalidation](./query-invalidation.md) is common. In these cases, you will need to manage your query keys in a more structured way to prevent typos: ```ts twoslash export const DOCUMENT_QUERY_KEYS = { root: ['documents'] as const, byId: (id: string) => [...DOCUMENT_QUERY_KEYS.root, id] as const, byIdWithComments: (id: string) => [...DOCUMENT_QUERY_KEYS.byId(id), { withComments: true }] as const, } export const DOCUMENT_COMMENT_QUERY_KEYS = { root: ['documents', 'comments'] as const, byId: (id: string) => [...DOCUMENT_COMMENT_QUERY_KEYS.root, id] as const, byIdWithReplies: (id: string) => [...DOCUMENT_COMMENT_QUERY_KEYS.root, id, { replies: true }] as const, } ``` * `root` is the root key of the query and can contain multiple values like `['documents', 'comments']` * Reuse the keys to keep the hierarchy and **avoid typos** * `as const` improves the type inference You can then reuse these keys in your queries and when interacting with the cache: ```ts twoslash //---cut-start--- import { useQuery, useQueryCache } from '@pinia/colada' import { useRoute } from 'vue-router' import { getDocumentById } from '@/api/documents' import { DOCUMENT_QUERY_KEYS } from '@/queries/documents' //---cut-end--- const route = useRoute() const { state } = useQuery({ key: () => DOCUMENT_QUERY_KEYS.byId(route.params.docId as string), query: () => getDocumentById(route.params.docId as string), }) const queryCache = useQueryCache() queryCache.invalidateQueries({ key: DOCUMENT_QUERY_KEYS.byId(route.params.docId as string), }) ``` This ensures that you are using the same keys everywhere and avoids typos. It also makes it easier to change the keys in the future if needed. It is recommended to use key factories in any project that has more than a couple of queries or if you to interact with the query cache. ## Typing query keys When interacting with the query cache, you can provide a type parameter to enforce the data type: ```ts{4} twoslash import { useQueryCache } from '@pinia/colada' // ---cut-start--- import type { Doc } from './api/documents' // ---cut-end--- const queryCache = useQueryCache() const docList = queryCache.getQueryData(['documents', 'list']) // ^? // ``` While this helps with types, it's not only manual but also not strict. If we define query options with `defineQueryOptions`, the *key* will be automatically *tagged* with type information inferred from `query`, making it easier and stricter to use: ```ts{3-6,9} twoslash import { useQueryCache, defineQueryOptions } from '@pinia/colada' // ---cut-start--- import { getDocumentList } from './api/documents' // ---cut-end--- const documentList = defineQueryOptions({ key: ['documents', 'list'], query: () => getDocumentList(), }) const queryCache = useQueryCache() const docList = queryCache.getQueryData(documentList.key) // ^? ``` ::: tip Combine this with the [key factories](#Managing-query-keys-key-factories-) to have a strictly type-safe solution that scales! ::: Differently from `useQuery()`, `defineQueryOptions` does not accept `MaybeRefOrGetter` versions of the properties (e.g. a function getter for the `key`), instead see [dynamic keys](#Dynamic-typed-keys) below. ### Dynamic typed keys `defineQueryOptions` also allows you to define options with a function to create dynamic keys. In this case, it returns a function instead of an object: ```ts{4-7,10} twoslash import { defineQueryOptions, useQueryCache } from '@pinia/colada' import { getDocumentById } from './api/documents' const documentByIdQuery = defineQueryOptions((id: string) => ({ key: ['documents', id], query: () => getDocumentById(id), })) const queryCache = useQueryCache() const docById = queryCache.getQueryData(documentByIdQuery('some-id').key) // ^? // ``` You are free to type the arguments of the function as you like, if you need multiple variables, use an object and destructure it: ```ts{2} const documentByIdQuery = defineQueryOptions( ({ id, withComments = false }: { id: string; withComments?: boolean }) => ({ key: ['documents', id, { comments: withComments }], query: () => getDocumentById(id, withComments), }), ) ``` To use dynamic query options, pass a single function to `useQuery`: ```vue{6} twoslash ``` You can also [pass any extra options](./queries.md#Passing-extra-options-defineQueryOptions-) to `useQuery` that will override the ones defined in `defineQueryOptions`. --- --- url: /guide/query-meta.md --- # Query Meta Attach metadata to any query and access it through *entries* in plugins and hooks. This allows you to implement cross-cutting concerns like error handling, logging, or analytics. ## Basic Usage Add meta to a query by passing an object to `meta` option: ```ts twoslash import { useQuery } from '@pinia/colada' useQuery({ key: ['user', 1], query: () => fetch('/api/user/1').then((r) => r.json()), meta: { errorMessage: 'Failed to load user', }, }) ``` You can put anything you want in meta, but if you are doing SSR, you will need to ensure that [the meta is serializable](#SSR). You can also use a function (or a ref) to dynamically and **lazily** compute it ```ts import { useQuery } from '@pinia/colada' const id = ref(1) useQuery({ key: () => ['user', id.value], query: () => fetch('/api/user/1').then((r) => r.json()), meta: () => ({ isFirst: id.value === 1, }), }) ``` ::: tip Meta is computed **only once**, when the entry is created. The main advantage of using a function or ref is to delay the creation of this meta object until the entry is created. ::: ## TypeScript Define your meta type globally by extending the `TypesConfig` interface: ```ts // types-extension.d.ts import '@pinia/colada' declare module '@pinia/colada' { interface TypesConfig { queryMeta: { errorMessage?: string logLevel?: 'debug' | 'info' | 'warn' | 'error' } } } export {} ``` Now your meta will be fully typed across your application. ## Using Meta in Query Hooks The most common use case for meta is to access it in global hooks like [plugins](../plugins/writing-plugins.md). You could for example notify the user with toast **on specific errors** with the `PiniaColadaQueryHooksPlugin`: ```ts twoslash import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' // ---cut-start--- import type { App } from 'vue' declare const app: App declare const toast: Record<'debug' | 'info' | 'warn' | 'error', (msg: string) => void> // ---cut-end--- app.use(PiniaColada, { plugins: [ PiniaColadaQueryHooksPlugin({ onError(error, entry) { if (entry.meta?.errorMessage) { toast.error(entry.meta.errorMessage) } }, }), ], }) ``` ## SSR Meta is serialized during SSR, so it **must be serializable** (plain objects, arrays, primitives, etc ) or **explicitly skipped** during SSR. ```ts twoslash import { useQuery } from '@pinia/colada' // ---cut-start--- import { getAllContacts, type Contact } from '@/api/contacts' declare const myCallback: (error: unknown) => void // ---cut-end--- // βœ… SSR-safe useQuery({ key: ['contacts'], query: getAllContacts, meta: { priority: 'high' }, }) // ❌ Not SSR-safe useQuery({ key: ['contacts'], query: getAllContacts, meta: { onError: () => console.log('error'), // Functions don't serialize }, }) // βœ… Skip non-serializable data during SSR useQuery({ key: ['contacts'], query: getAllContacts, meta: { onError: import.meta.env.SSR ? undefined : myCallback, }, }) ``` ## Access Patterns Meta is stored in two places: * `entry.meta` - The resolved value (computed once when entry is created) * `entry.options.meta` - The original function/ref/raw value ```ts twoslash import { useQueryCache, useQuery } from '@pinia/colada' useQuery({ key: ['time'], query: () => fetch('/api/time').then((r) => r.json()), meta: () => ({ timestamp: Date.now() }), }) // ---cut-start--- const queryCache = useQueryCache() // ---cut-end--- // In your plugin queryCache.$onAction(({ name, after, args }) => { if (name === 'extend') { const [entry] = args console.log(entry.meta) // { timestamp: 1234567890 } console.log(entry.options?.meta) // [Function: metaFn] } }) ``` --- --- url: /guide/paginated-queries.md --- # Paginated Queries Paginated queries are a way to retrieve a subset of the results of a query. In Pinia Colada all you need for paginated queries is to **pass the page to the `key` function**. ```vue{7,9} twoslash ``` Make sure you provide a *function* to `key` and not just a plain value. This will ensure the key is reactive and changes when the page changes. ## Keeping the old data When navigating through pages, as the `key` updates, **a new cache entry is created**. This also means that `state.data` becomes `undefined` when navigating to a new page. To keep the old data while fetching the new one, you can use the `placeholderData` option. ```vue{10} twoslash ``` `placeholderData` is really convenient for paginated queries because it will return the `state.status` as `success` without actually changing the cache state πŸ’‘. This also means they are not serialized during SSR. The `asyncStatus` will still be `loading` until the query resolves. ## Infinite scroll Regular pagination with `useQuery()` treats each page as an independent cache entry. For infinite scrolling / β€œload more”, you typically want to merge pages into a single cache entry. Use `useInfiniteQuery()` for that. See [Infinite Queries](./infinite-queries.md) for more details. --- --- url: /guide/infinite-queries.md --- # Infinite Queries Infinite queries let you load and merge multiple pages into a **single cache entry**. This is different from paginated queries with `useQuery()` where each page is a separate cache entry (page is part of the `key`). If you haven’t read it yet, start with [Paginated Queries](./paginated-queries.md). ## When to use `useInfiniteQuery()` isn't for every use case. It’s ideal when: * you want infinite scrolling / "load more" behavior (most common) * you want all loaded pages to be invalidated/refetched as one unit * you don't want pages to be garbage-collected independently ## Key design With infinite queries, **the page/cursor is not part of the query key**. Put *filters* in the key (search text, sort order, user id…), but not the `pageParam`: ```ts useInfiniteQuery({ key: () => ['feed', { search: search.value }], // ... }) ``` Changing the key resets the infinite query as it creates a new cache entry. ## Page-based example ```ts import { useInfiniteQuery } from '@pinia/colada' const { data, asyncStatus, hasNextPage, loadNextPage, } = useInfiniteQuery({ key: ['feed'], initialPageParam: 1, query: ({ pageParam }) => fetch(`/api/feed?page=${pageParam}`).then(r => r.json()), getNextPageParam: (lastPage) => lastPage.nextPage ?? null, }) ``` The result is shaped like: * `data.value.pages`: array of pages * `data.value.pageParams`: the associated params used to fetch each page Each time you call `loadNextPage()`, the new page is merged into the same cache entry as the key is the same. To make it work, you must define how to get the next page param with `getNextPageParam` (or `getPreviousPageParam` for backward pagination). In this example, we use the key `nextPage` from the latest response. So, the API returns something like: ```json { "items": [/* page items */], "nextPage": 2 } ``` When `nextPage` is `null`, `hasNextPage` becomes `false` and `loadNextPage()` does nothing. ## Cursor-based example If your API returns a cursor, you can use that as the page param instead of a page number: ```ts useInfiniteQuery({ key: ['notifications'], initialPageParam: null as string | null, query: ({ pageParam }) => api.listNotifications({ cursor: pageParam }), getNextPageParam: (lastPage) => lastPage.nextCursor ?? null, }) ``` ## Load more example You may want a feed-like behavior where you load more items as the user scrolls down. This can be achieved using an `IntersectionObserver` that triggers `loadNextPage()` when the user scrolls near the bottom of the list. ```vue twoslash ``` ## Limiting memory with `maxPages` For very long feeds (or chat-like UIs), you can keep only a fixed number of pages in memory: ```ts useInfiniteQuery({ key: ['chat', conversationId], initialPageParam: 1, maxPages: 10, // ... }) ``` This will keep only the 10 most recent pages in memory. When a new page is loaded, the oldest page is removed from the cache. ## Invalidation and refetch Invalidating the infinite query invalidates the **whole entry**. Refetching will refetch the loaded pages. ## SSR SSR works like any other query: the infinite query is serialized as one entry. Typically you’ll prefetch the initial page, and optionally load more pages server-side if your UI requires it. --- --- url: /guide/mutations.md --- # Mutations Mutations allow us to trigger and track the status of an async operation meant to have side effects. While [queries](./queries.md) are meant to **read** data, mutations are meant to **write** data. In terms of REST, queries *usually* handle `GET` requests and mutations handle `POST`, `PUT`, `PATCH`, and `DELETE` requests (without limiting you to do so). Similarly to queries, mutations are defined with the `useMutation()` composable. ## Basic usage A basic mutation is usually defined with a single property, `mutation`, which is a function that performs the async operation. ```vue twoslash ``` In the example above, it's worth noting that the `mutation` function has one parameter, `todoText`, which is passed to the `createTodo` function when the form is submitted. We could also directly use the `todoText.value` directly in the `mutation` function. The main advantage of defining a parameter is that it makes it available in mutation hooks, like `onSuccess` or `onError`. In general, it's a good practice to define the parameters of the mutation function. They will also get reflected on the `variables` property of the mutation. ```vue twoslash ``` ## Mutations ❀️ Queries Using mutations allows us to easily keep the changed data up to date. We have multiple strategies to do so: * [Invalidate queries](./query-invalidation.md#Invalidation-in-Mutation-Hooks) that depend on the data that was changed. * [Optimistic updates](./optimistic-updates.md) to update the UI before the mutation is completed. ## Reusable mutations [Similarly to queries](../advanced/reusable-queries.md), mutations can be defined and reused across components with the `defineMutation()` function. ::: code-group ```ts [mutations/todos.ts] twoslash // NOTE: to sync with mutations.md // @filename: mutations/todos.ts // ---cut-before--- import { ref } from 'vue' import { defineMutation, useMutation } from '@pinia/colada' export const useCreateTodo = defineMutation(() => { const todoText = ref('') const { mutate, ...mutation } = useMutation({ mutation: (text: string) => fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }), }), }) return { ...mutation, // we can still pass the todoText to the mutation so it appears in plugins // and other places createTodo: () => mutate(todoText.value), // expose the todoText ref todoText, } }) ``` ```vue [components/CreateTodo.vue] twoslash ``` ::: In the scenario above, the `todoText` is created only once and shared across all components that use the `useCreateTodo` composable. When you just want to organize your mutations, you can also pass an object of options to `defineMutation()`: ```ts twoslash // src/mutations/todos.ts import { defineMutation } from '@pinia/colada' export const useCreateTodo = defineMutation({ mutation: (todoText: string) => fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text: todoText }), }), }) ``` ## Executing mutations Mutations expose two different methods for executing the mutation: * `mutate`: The main method to execute the mutation. It accepts the same parameters as the mutation function and does not return anything. * `mutateAsync`: A method that returns a promise that resolves when the mutation is completed. It accepts the same parameters as the mutation function. ```vue twoslash ``` Most of the time, using `mutate` should be easier to use as it catches any errors thrown by the mutation. On the other hand `mutateAsync` is useful when you need to handle the errors yourself or to wait for the mutation to complete. ## Mutation state The mutation returns similar properties to queries, like `state`, `data`, `error`, `status`, `asyncStatus`, etc. However, mutations are, by default, not global. ## Hooks ### Global hooks Global hooks like `onMutate`, `onSuccess`, `onError`, and `onSettled` can be set through `mutationOptions` when installing `PiniaColada`. If you want to **extend** what `useMutation()` returns (e.g. add `mutatedAt`, counters, logging), this is done through the plugin system. See [Plugins](../plugins/writing-plugins.md). ## Best Practices --- --- url: /guide/error-handling.md --- # Error Handling Queries and mutations expose `error` and `status` refs. Handle errors in the template, with hooks, or via global plugins. ## Error state in queries `useQuery()` returns an `error` ref and a `status` ref. When a query function throws (or rejects), `status` becomes `'error'` and `error` holds the thrown value. See the [status table in Queries](./queries.md#Foundations) for the full lifecycle. ```vue twoslash ``` ::: tip Previous data is preserved on refetch failure When a query refetch fails, the new `error` is set **but the previous `data` is kept**. This lets you show stale data alongside an error banner rather than replacing the entire UI. ::: ## Error state in mutations `useMutation()` exposes the same `error` and `status` refs. The difference is how each method handles errors: * **`mutate()`** β€” swallows errors. The error is set on the `error` ref and passed to `onError` hooks, but it is **not** rethrown. Use hooks to react. * **`mutateAsync()`** β€” rethrows the error after running hooks. Wrap it in `try/catch`. ```vue twoslash ``` Calling `reset()` clears the mutation's error and data, returning it to its initial pending state. ## Reacting to errors with hooks Use mutation hooks to handle errors outside the template. The `onError` hook receives the error, the variables passed to the mutation, and the context returned by `onMutate`: ```ts twoslash import { useMutation } from '@pinia/colada' // ---cut-start--- declare const toast: { error: (msg: string) => void } // ---cut-end--- const { mutate } = useMutation({ mutation: (name: string) => fetch('/api/profile', { method: 'PATCH', body: JSON.stringify({ name }), }), onError(error, vars, context) { toast.error(`Failed to update name to "${vars}"`) }, onSettled() { // Runs after both success and error }, }) ``` The `context` argument contains whatever you returned from `onMutate`, which is useful for [rolling back optimistic updates](./optimistic-updates.md) on failure. ## Global error handling ### Mutations Set global hooks for **all mutations** via `mutationOptions` when installing Pinia Colada: ```ts twoslash import { createApp } from 'vue' import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' // ---cut-start--- declare const toast: { error: (msg: string) => void } // ---cut-end--- const app = createApp({}) app.use(createPinia()) app.use(PiniaColada, { mutationOptions: { onError(error) { if (error instanceof Error) { toast.error(error.message) } else { // somebody threw something that isn't an Error console.error('Unexpected error:', error) } }, }, }) ``` ::: tip Avoid toast on every mutation error A global handler shouldn't show a toast for every failure. Filter by error type or use a less noisy UI (e.g. a status bar). ::: ### Queries `PiniaColadaQueryHooksPlugin` gives you global query error hooks. `onError` receives the error and entry, so you can read `entry.meta` for per-query context: ```ts twoslash import { createApp } from 'vue' import { createPinia } from 'pinia' import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' // ---cut-start--- declare const toast: { error: (msg: string) => void } // ---cut-end--- const app = createApp({}) app.use(createPinia()) app.use(PiniaColada, { plugins: [ PiniaColadaQueryHooksPlugin({ onError(_error, entry) { if (entry.meta?.errorMessage) { toast.error(entry.meta.errorMessage as string) } }, }), ], }) ``` Then in your queries, keep things declarative with `meta`: ```ts useQuery({ key: ['todos'], query: () => fetchTodos(), meta: { errorMessage: 'Failed to load the todo list', }, }) ``` See the [Query Hooks plugin](../plugins/official/query-hooks.md) for the full API and [Writing Plugins](../plugins/writing-plugins.md) if you need lower-level access. ## Handling non-throwing API errors Some APIs (like `fetch` or typed API clients) don't throw on non-2xx responses β€” they return a response with a status code. Pinia Colada only treats **thrown or rejected** values as errors. Options: ### Throw in the query function Check the response and throw explicitly. ```ts twoslash import { useQuery } from '@pinia/colada' class ApiError extends Error { constructor( public status: number, message: string, ) { super(message) } } const { data, error } = useQuery({ key: ['todos'], query: async () => { const response = await fetch('/api/todos') if (!response.ok) { throw new ApiError(response.status, response.statusText) } return response.json() }, }) ``` ### Keep errors in data Keep the full response in `data` and check the status in your template. This fits typed API clients (ts-rest, oRPC) with error shapes: ```ts const { data } = useQuery({ key: ['todos'], query: async () => { const result = await typedApi.getTodos() // result.status is 200 | 404 | 500, etc. return result }, }) ``` ```vue-html ``` ### Intercept with a plugin Or intercept `setEntryState` in a plugin and convert non-OK responses into error states: ```ts twoslash // plugin-api-errors.ts import type { PiniaColadaPlugin } from '@pinia/colada' // ---cut-start--- function isApiError(data: unknown): data is { status: number } { return ( !!data && typeof data === 'object' && 'status' in data && typeof data.status === 'number' && data.status >= 400 ) } // ---cut-end--- export const PiniaColadaApiErrorPlugin: PiniaColadaPlugin = ({ queryCache }) => { queryCache.$onAction(({ name, args, after }) => { if (name === 'setEntryState') { after(() => { const entry = args[0] const state = entry.state.value if (state.status === 'success' && isApiError(state.data)) { entry.state.value = { status: 'error', error: new Error(`API error: ${state.data.status}`), data: state.data, } } }) } }) } ``` ## Retrying failed queries The [Retry plugin](../plugins/official/retry.md) adds automatic retries with configurable count, delay, and filtering by error type. It can retry failed queries before surfacing errors. ## Typing errors ### Default error type By default, all errors are typed as `Error` (via the `ErrorDefault` type). You can change this globally by augmenting the `TypesConfig` interface: ```ts twoslash import '@pinia/colada' export interface MyCustomError extends Error { code: number } declare module '@pinia/colada' { interface TypesConfig { defaultError: MyCustomError } } ``` After this, `error` is typed as `MyCustomError` unless overridden locally. ::: tip Strict error handling Set `defaultError` to `unknown` to force explicit narrowing. ::: ### Narrowing with `status` The `state` returned by `useQuery()` and `useMutation()` is a discriminated union on `status`. Checking `status` narrows `error` automatically: ```ts twoslash import { useQuery } from '@pinia/colada' // @errors: 18046 const { state } = useQuery({ key: ['todos'], query: () => fetch('/api/todos').then((r) => r.json()), }) const current = state.value if (current.status === 'error') { // current.error is narrowed to `ErrorDefault` (Error by default) console.error(current.error.message) } ``` ### Local narrowing with `instanceof` Errors can't be typed per-query. Narrow them in code and keep a catch-all branch: ```vue-html ``` --- --- url: /guide/query-invalidation.md --- # Query Invalidation Query invalidation is crucial for maintaining up-to-date data in your application. Typically, you should invalidate queries following mutations to ensure the cache stays synchronized with the server. However, you can also manually invalidate queries using the [Query Cache](../advanced/query-cache.md). Let's cover both methods. ## Invalidation via the Query Cache You can directly invalidate queries using the query cache. This marks matching queries as stale, prompting a refetch if it's active (i.e., currently used by a component). You can filter queries by `key`, `status`, `active`, and more. ```ts twoslash import { useQueryCache } from '@pinia/colada' const queryCache = useQueryCache() // Invalidate a specific query and its children queryCache.invalidateQueries({ key: ['todos'] }) // Invalidate a specific query only queryCache.invalidateQueries({ key: ['todos'], exact: true }) // Refetch all active queries queryCache.invalidateQueries() ``` You can also use a `predicate` function for custom filtering logic. It receives a query entry and returns a boolean indicating whether the query should be invalidated: ```ts queryCache.invalidateQueries({ // Invalidate queries that key starts with `users` or `todos` predicate: (entry) => entry.key[0] === 'users' || entry.key[0] === 'todos', }) ``` By default, `invalidateQueries()` invalidates all queries (both active and inactive) but only refetches active queries. Inactive queries are marked as stale and will be refetched when they become active again. You can change this behavior by passing a second parameter to refetch all queries: ```ts queryCache.invalidateQueries({ key: ['todos'] }, 'all') ``` `invalidateQueries()` never refetches queries paused with `enabled: false`, even with `'all'`. They are still marked as stale. Invalidating a query also [cancels](./cancelling-queries.md) its pending request. Passing `false` as the second parameter marks the queries as stale without refetching any of them, so a query that was loading stops and does not restart until something refreshes it. To leave loading queries alone, filter them out: ```ts queryCache.invalidateQueries( { key: ['todos'], predicate: (entry) => entry.asyncStatus.value !== 'loading' }, false, ) ``` ::: info The `useQueryCache` composable grants access to the query cache. It can be used within your components' setup function and other contexts where `inject()` is available, such as Pinia Stores and Router navigation guards. Note that it cannot be invoked in the global scope or within a component method. ::: ## Invalidation in Mutation Hooks When we mutate a resource, we can be sure that the data has changed. This is a good time to invalidate queries that depend on that resource. You can use the `onSettled` callback to invalidate queries after the mutation completes. This ensures that the cache is updated with the latest data. ```vue twoslash ``` ### To `await` or not to `await` In mutations, it's possible to `await` (or return a promise) within the different hooks. This will effectively delay the resolution or rejection of the mutation and its `asyncStatus`: ```ts const queryCache = useQueryCache() const { mutate } = useMutation({ mutation: (text: string) => createTodo(text), onSettled: async () => // The mutation will resolve/reject after the related queries have been fetched again await queryCache.invalidateQueries({ key: ['todos'], exact: true }), }) ``` This allows you to display a loading state until the queries are refetched. ### Optimistic Updates You can also combine query invalidation with optimistic updates like this: ```ts import { useMutation, useQueryCache } from '@pinia/colada' import { createTodo } from './api/todos' const queryCache = useQueryCache() const { mutate } = useMutation({ mutation: (text: string) => createTodo(text), onMutate: (text: string) => { const previousTodos = queryCache.getQueryData(['todos']) queryCache.setQueryData(['todos'], [...(previousTodos || []), { text }]) // return anything you want to reuse in other hooks return { previousTodos } }, onError: (error, variables, { previousTodos }) => { // rollback to the previous state queryCache.setQueryData(['todos'], previousTodos) }, onSettled: () => { // invalidate the query to refetch the new data queryCache.invalidateQueries({ key: ['todos'] }) }, }) ``` Optimistic Updates enhance user experience by immediately reflecting changes in the UI before the mutation completes. However, they require additional handling for potential errors. For more details, refer to the [Optimistic Updates](./optimistic-updates.md) guide. --- --- url: /guide/optimistic-updates.md --- # Optimistic updates Optimistic updates are a way to update the UI **before the mutation has completed**, optimistically assuming that the mutation will succeed. This is a way to provide a more responsive UI and a better user experience. Pinia Colada provides multiple ways to implement optimistic updates, depending on the use case. ## Via the cache Updating the cache directly is the most efficient way to implement optimistic updates because you are collocating the optimistic update with the mutation itself. Since you are touching the cache directly, any query relying on the updated data will automatically reflect the changes. However, this also requires handling the **rollback** in case of errors. Here is a **complete example** of an optimistic update for the details of a contact: ```ts twoslash import { useMutation, useQueryCache } from '@pinia/colada' import { patchContact } from '@/api/contacts' import type { Contact } from '@/api/contacts' const queryCache = useQueryCache() const { mutate } = useMutation({ mutation: patchContact, // `contactInfo` type is inferred from the mutation function onMutate(contactInfo) { // get the current contact from the cache, we assume it exists const oldContact = queryCache.getQueryData(['contact', contactInfo.id])! const newContact: Contact = { // we merge both objects to have a complete contact ...oldContact, ...contactInfo, } // update the cache with the new contact queryCache.setQueryData(['contact', newContact.id], newContact) // we cancel (without refetching) all queries that depend on the contact queryCache.cancelQueries({ key: ['contact', newContact.id] }) // pass the old and new contact to the other hooks return { oldContact, newContact } }, // on both error and success onSettled(_data, _error, _vars, { newContact }) { // `newContact` can be undefined if the `onMutate` hook fails if (newContact) { // invalidate the query to refetch the new data queryCache.invalidateQueries({ key: ['contact', newContact.id] }) } }, onError(err, contactInfo, { newContact, oldContact }) { // before applying the rollback, we need to check if the value in the cache // is the same because the cache could have been updated by another mutation // or query if (newContact === queryCache.getQueryData(['contact', contactInfo.id])) { queryCache.setQueryData(['contact', contactInfo.id], oldContact) } // handle the error console.error(`An error occurred when updating a contact "${contactInfo.id}"`, err) }, // Depending on your code, this `onSuccess` hook might not be necessary onSuccess(contact, _contactInfo, { newContact }) { // update the contact with the information from the server // since we are invalidating queries, this allows us to progressively queryCache.setQueryData(['contact', newContact.id], contact) }, }) ``` Here is a **complete example** of an optimistic update of a list of todos when creating a new todo item: ```ts twoslash import { useMutation, useQueryCache } from '@pinia/colada' import { createTodo } from './api/todos' import type { TodoItem } from './api/todos' const queryCache = useQueryCache() const { mutate } = useMutation({ mutation: (text: string) => createTodo(text), onMutate(text) { // save the current todo list const oldTodoList = queryCache.getQueryData(['todos']) // keep track of the new todo item const newTodoItem: TodoItem = { text, // we need to fill every required field id: crypto.randomUUID(), } // create a copy of the current todo list with the new todo const newTodoList: TodoItem[] = [ ...(oldTodoList || []), newTodoItem, ] // update the cache with the new todo list queryCache.setQueryData(['todos'], newTodoList) // we cancel (without refetching) all queries that depend on the todo list // to prevent them from updating the cache with an outdated value queryCache.cancelQueries({ key: ['todos'] }) // pass the old and new todo list to the other hooks // to handle rollbacks return { newTodoList, oldTodoList, newTodoItem } }, // on both error and success onSettled() { // invalidate the query to refetch the new data queryCache.invalidateQueries({ key: ['todos'] }) }, onError(err, _title, { oldTodoList, newTodoList }) { // before applying the rollback, we need to check if the value in the cache is the same // because the cache could have been updated by another mutation or query if (newTodoList === queryCache.getQueryData(['todos'])) { queryCache.setQueryData(['todos'], oldTodoList) } // handle the error console.error('An error occurred when creating a todo:', err) }, onSuccess(todoItem, _vars, { newTodoItem }) { // update the todo with the information from the server // since we are invalidating queries, this allows us to progressively // update the todo list even if the user is submitting multiple mutations // successively const todoList = queryCache.getQueryData(['todos']) || [] // find the todo we added in `onMutate()` and replace it with the one from the server const todoIndex = todoList.findIndex((t) => t.id === newTodoItem.id) if (todoIndex >= 0) { // Replace the whole array to trigger a reactivity update // we could also use `.toSpliced()` in modern environments const copy = todoList.slice() copy.splice(todoIndex, 1, todoItem) queryCache.setQueryData(['todos'], copy) } }, }) ``` As you see, depending on the mutation, you might need to update multiple queries in different ways. Optimistic updates are a very powerful tool, but they also require more care to handle errors and edge cases. Because of this, Pinia Colada provides a low level API to cater to all use cases! ::: tip Type Safety In large projects, you might face two issues: * `key` isn't typed, so it's very easy to have a typo in a query key: *was it **todos** or **todo**?* * You have to manually pass a type parameter (here `TodoItem[]`) to `getQueryData()` and `setQueryData()` to ensure type safety This can be solved by using [Key Factories](./query-keys.md#Managing-query-keys-key-factories-) and by [defining options with `defineQueryOptions()`](./query-keys.md#Typing-query-keys). ::: ## Via the UI Handling the optimistic update directly in the UI is the simplest approach. This can be achieved by updating the UI immediately when the mutation is called. Combine the `variables` property of the mutation with invalidating affected queries to implement this method effectively. This approach is possible when the mutation lives close to the query it updates, it allows to show the pending changes directly in the UI that renders a query: ```vue{12-18,33-35} twoslash ``` In this example, while the mutation is loading, the new todo is displayed in the list. The `variables` property of the mutation contains the last variables passed to the mutation. It's up to you if you want to display them differently or not: ```vue-html
  • {{ newTodo }}
  • ``` When doing this, it's important to remember to invalidate the query after the mutation has completed with `await queryCache.invalidateQueries()`. This will ensure that the query is refetched with the new data. By awaiting the invalidation, the mutation will stay in the loading state until all related queries have been refetched. ### When mutation is not collocated with the query When the mutation is not collocated with the query it updates, you can still use the mutation state next to the query. In this case, **you must specify a key for the mutation** so it can be referenced by `mutationCache.getEntries()`: ```ts twoslash import { useMutation, useQueryCache } from '@pinia/colada' import { createTodo } from './api/todos' const queryCache = useQueryCache() const { mutate, isLoading, variables: newTodo, } = useMutation({ key: ['createTodo'], mutation: (text: string) => createTodo(text), onSettled: () => queryCache.invalidateQueries({ key: ['todos'] }), }) ``` Then, you can use `mutationCache.getEntries()` to access the mutation state in another component: ```ts import { useMutationCache, useQuery } from '@pinia/colada' import { getTodoList } from './api/todos' const { data: todoList } = useQuery({ key: ['todos'], query: () => getTodoList(), }) const mutationCache = useMutationCache() const mutationState = computed(() => { const mutation = mutationCache.getEntries({ key: ['createTodo'] })[0] return { isLoading: mutation?.asyncStatus.value === 'loading', variables: mutation?.vars, } }) ``` --- --- url: /nuxt.md --- # Nuxt Pinia Colada integrates seamlessly with Nuxt via its dedicated module. It provides advanced caching, automatic deduplication, mutations with optimistic updates, and cross-component data sharing that goes beyond what Nuxt's built-in data fetching offers. ## Choosing Your Data Fetching Approach Both Nuxt's native composables (`useFetch`/`useAsyncData`) and Pinia Colada can fetch data with SSR support. Here's when to use each: | Feature | Nuxt Native | Pinia Colada | | ---------------------- | -------------------------- | -------------------------------- | | Best for | Simple page-level data | Complex app-wide state | | SSR | `await` required | Automatic via `onServerPrefetch` | | Parallel requests | Manual via `Promise.all` | Automatic | | Caching | Manual via `getCachedData` | Automatic + deduplication + GC | | Mutations | Manual | Built-in `useMutation` | | Optimistic updates | Manual | Built-in | | Scope | Pages (prop drilling) | Any component | | Stale-while-revalidate | Manual | Built-in | ::: tip When to use Nuxt's native `useFetch`/`useAsyncData` * Simple page-specific data that isn't shared across components * One-off API calls without complex caching needs * Single requests without parallel fetching ::: ::: tip When to use Pinia Colada * Data shared across multiple components or pages * When you need automatic cache invalidation and garbage collection * When you have parallel requests within a component * If you need cache persistence * Mutations with optimistic updates * Complex apps with lots of interdependent data * When deduplication and stale-while-revalidate matter ::: ## Installation ::: code-group ```bash [pnpm] pnpm add @pinia/colada pnpm dlx nuxi module add @pinia/colada-nuxt ``` ```bash [npm] npm install @pinia/colada npx nuxi module add @pinia/colada-nuxt ``` ```bash [yarn] yarn add @pinia/colada yarn dlx nuxi module add @pinia/colada-nuxt ``` ::: Or manually by installing it and adding it to your `nuxt.config.ts`: ::: code-group ```bash [pnpm] pnpm add @pinia/colada @pinia/colada-nuxt ``` ```bash [npm] npm install @pinia/colada @pinia/colada-nuxt ``` ```bash [yarn] yarn add @pinia/colada @pinia/colada-nuxt ``` ::: ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/colada-nuxt'], }) ``` ::: info Since Pinia Colada depends on Pinia, you also need to install its Nuxt module: ```bash npx nuxi module add pinia ``` ::: ## Devtools Use a version of `@pinia/colada-nuxt` with Devframe support. Older versions do not register the v2 panel, even with Nuxt DevTools 4. Install the devtools as a development dependency: ```sh pnpm add -D @pinia/colada-devtools ``` Use Nuxt DevTools 4 or later with the Vite builder and enable devtools in `nuxt.config.ts`: ```ts export default defineNuxtConfig({ modules: ['@pinia/nuxt', '@pinia/colada-nuxt'], devtools: { enabled: true }, }) ``` The Pinia Colada module registers the panel during development. Open Nuxt DevTools and select Pinia Colada. See the [devtools installation guide](./guide/installation.md#Pinia-Colada-Devtools) for other hosts. ## Configuration You can configure the Pinia Colada plugin by creating a `colada.options.ts` file at the root of your project. ```ts // colada.options.ts import type { PiniaColadaOptions } from '@pinia/colada' export default { // Options here } satisfies PiniaColadaOptions ``` These options will get passed to the `PiniaColada` Vue plugin. This allows you to add options like [plugins](./guide/installation.md#Plugins). ## SSR Without `await` **The key difference from Nuxt's data fetching: Pinia Colada doesn't require `await` for SSR.** With Nuxt's `useFetch`, you must `await` to block SSR and wait for data: ```vue ``` With Pinia Colada, `useQuery` uses `onServerPrefetch` internally, so queries automatically run and await on the server without explicit `await`: ```vue ``` ### How it works 1. On the server, `useQuery` automatically registers via `onServerPrefetch` 2. The query runs and awaits during server-side rendering 3. Data is serialized to the payload and hydrated on the client 4. No extra code neededβ€”it just works The module also installs the [`PiniaColadaSSRNoGc`](./guide/ssr.md#Garbage-collection-on-the-server) plugin and clears the cache after every render. This means SSG / `nuxt build` / test processes exit cleanly without waiting for `gcTime` to elapse, and entries don't leak across requests. ### When you still need `await` Using `await` with `useQuery` is still useful when you want to **block client side navigation** until data loads: ```vue ``` Without `await`, the page renders immediately (showing loading states), and data populates when ready. Another alternative is to use [Data Loaders](https://router.vuejs.org/data-loaders/colada/), which connect the data fetching lifecycle to Vue Router (and therefore Nuxt) navigation system. ## Migration Guide ### `useFetch` β†’ `useQuery` ```vue ``` Key differences: * `pending` β†’ `isPending` * Add a unique `key` for caching * Wrap the fetch in `query` option * Remove `await`: * SSR works automatically * Add `await refresh()` if you want to block navigation until data loads ### `useAsyncData` β†’ `useQuery` ```vue ``` ### Adding mutations Check out the full [Mutations Guide](./guide/mutations.md) for more details. ### Shared data across components **Before (Nuxt):** Pass data down via props from page components ```vue ``` **After (Pinia Colada):** Any component can access the same cached data ```vue ``` ```vue ``` Check the [Query Organization Guide](/guide/queries.md#Organizing-Queries) for best practices on organizing shared queries. ## Error Handling with SSR Standard JavaScript `Error` objects work out of the box. For custom error classes, you'll need to define [custom payload plugins](https://nuxt.com/blog/v3-4#payload-enhancements) to serialize them: ```ts // plugins/my-error.ts import { MyError } from '~/errors' export default definePayloadPlugin(() => { definePayloadReducer( 'MyError', // serialize the data we need as an array, object, or any serializable format (data) => data instanceof MyError && [data.message, data.customData], ) definePayloadReviver( 'MyError', // revive the data back to an instance of MyError ([message, customData]) => new MyError(message, customData), ) }) ``` --- --- url: /guide/ssr.md --- # Server Side Rendering Pinia Colada relies on Pinia for server side rendering. This means that you should follow [Pinia's SSR guide](https://pinia.vuejs.org/ssr/) to setup your server side rendering. On top of that, Pinia Colada relies on some custom data structures that requires special serialization. This is handled automatically in the [Nuxt module](../nuxt.md) but if you are using a custom SSR setup, you will need to handle this yourself by serializing and reviving the cache tree. You can check the [source code of the Nuxt module](https://github.com/posva/pinia-colada/blob/main/nuxt/src/runtime/plugin.ts) for inspiration ## Serialization If you use [devalue](https://github.com/sveltejs/devalue), you can follow their documentation [for custom types](https://github.com/sveltejs/devalue#custom-types). It should look something like this: ```ts import devalue from 'devalue' import { markRaw } from 'vue' import { isQueryCache, serializeQueryCache, hydrateQueryCache } from '@pinia/colada' const stringified = devalue.stringify(useQueryCache(pinia), { PiniaColada_TreeMapNode: (data: unknown) => isQueryCache(data) && serializeQueryCache(data), }) const revivedData = devalue.parse(stringified, { PiniaColada_TreeMapNode: (data: ReturnType) => // We will use a custom hydration function data, }) // we need to install Pinia and PiniaColada before hydrating the cache app.use(pinia) app.use(PiniaColada) hydrateQueryCache(useQueryCache(pinia), revivedData) ``` ## Garbage collection on the server By default, Pinia Colada schedules a `setTimeout` to garbage collect cache entries after `gcTime`. On the server this is undesirable for two reasons: * The pending timer keeps the Node.js process alive, which can hang `node`-based SSG / build pipelines for the duration of `gcTime` after rendering completes. * The closure inside the timer retains the entry in memory across requests. For server-side rendering you should disable GC on the server and clear the cache after each render. Pinia Colada exports the `PiniaColadaSSRNoGc` plugin for the first part: ```ts import { PiniaColada, PiniaColadaSSRNoGc, useQueryCache } from '@pinia/colada' app.use(PiniaColada, { plugins: import.meta.env.SSR ? [PiniaColadaSSRNoGc()] : [], }) // after rendering each request: useQueryCache(pinia).caches.clear() ``` The plugin forces `gcTime: false` on every query and mutation entry, so even per-call `useQuery({ gcTime: 30_000 })` overrides won't schedule a timer on the server. The [Nuxt module](../nuxt.md) wires both of these for you automatically. ## Lazy queries By passing the `enabled` option we can control whether the query should be executed or not. This is useful in many scenarios, for example, when you want to avoid fetching data on the server side. ```ts import { useQuery } from '@pinia/colada' useQuery({ key: ['restaurants', { search: true }], query: () => findRestaurants(), // could also be `!import.meta.env.SSR` in Vite environments enabled: typeof document !== 'undefined', }) ``` This will prevent the query from being executed and awaited on the server side. ## Error Handling with SSR Using devalue allows you to handle errors too. If you want to SSR errors, you will need to also handle their serialization ```ts import devalue from 'devalue' import { MyError } from '~/errors' const stringified = devalue.stringify(error, { MyError: (data: unknown) => data instanceof MyError && [data.message, data.customData], }) const revivedData = devalue.parse(stringified, { MyError: (data: [string, unknown]) => new MyError(data[0], data[1]), }) ``` --- --- url: /plugins.md --- # Plugins Plugins let you extend Pinia Colada globally (retries, refetch policies, persistence, global hooks, logging…) without changing every `useQuery()` / `useMutation()` call. A plugin is an install-time function registered when you install Pinia Colada: ```ts import { PiniaColada } from '@pinia/colada' app.use(PiniaColada, { plugins: [ // plugin factories return a plugin function // Some plugins are built into `@pinia/colada`, others are separate packages. ], }) ``` ## Using plugins Most plugins follow the same workflow: 1. Install the package (if it’s not built-in) 2. Add it to the `plugins` array 3. Optionally configure it globally (plugin options) and/or locally (per query/mutation options) Plugin order matters: if multiple plugins hook the same cache actions, they run in installation order. ## Official and Community plugins See [Official plugins](./official/index.md) and [Community plugins](./community.md). ## Writing plugins If you want to create your own plugin (or understand how existing ones work), see [Writing plugins](./writing-plugins.md). --- --- url: /plugins/official/query-hooks.md --- # Query Hooks `PiniaColadaQueryHooksPlugin` centralizes side-effect handling (toasts, logging, analytics…). ## Install This plugin is built-in, so you don't have to install it separately. ## Enable ```ts import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' app.use(PiniaColada, { plugins: [ PiniaColadaQueryHooksPlugin({ // ... }), ], }) ``` ### Keep `useQuery` declarative To decouple side effects from data logic, a common pattern is to keep queries declarative and pass a `meta` field: ```ts useQuery({ key: ['todos'], query: () => fetchTodos(), meta: { errorMessage: 'Failed to load todos', }, }) ``` Then configure the plugin once to use the `meta.errorMessage` for global error handling: ```ts import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' app.use(PiniaColada, { plugins: [ PiniaColadaQueryHooksPlugin({ onError: (_error, entry) => { if (entry.meta?.errorMessage) { toast.error(entry.meta.errorMessage) } }, }), ], }) ``` ## Configuration You can only configure the plugin globally, not per query: * `onError`: called when a query fails. * `onSuccess`: called when a query succeeds. * `onSettled`: called when a query settles, regardless of the outcome. ## Links * API reference: [/api](/api/@pinia/colada/functions/PiniaColadaQueryHooksPlugin.html) --- --- url: /plugins/official/auto-refetch.md --- # Auto refetch `@pinia/colada-plugin-auto-refetch` automatically refetches queries on an interval, typically when data is considered stale. ## Install ```bash npm i @pinia/colada-plugin-auto-refetch ``` ## Enable ```ts import { PiniaColada } from '@pinia/colada' import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch' app.use(PiniaColada, { plugins: [ PiniaColadaAutoRefetch({ // global default (can be overridden per-query) autoRefetch: true, }), ], }) ``` ## Configuration You can configure auto-refetch globally (plugin option) and/or per query. * `autoRefetch: false` disables it * `autoRefetch: true` enables it (uses `staleTime` to schedule) * `autoRefetch: number` uses a fixed interval in milliseconds * `autoRefetch: (state) => boolean | number` enables/disables (or chooses an interval) based on the current query state Per-query example: ```ts useQuery({ key: ['todos'], query: fetchTodos, staleTime: 10_000, autoRefetch: true, }) ``` ::: warning SSR This plugin schedules timers and is effectively client-only. It will not schedule refetches during SSR. ::: ## Notes * If you set `autoRefetch: true`, also set a `staleTime`. Without a `staleTime`, there is nothing to schedule. * If you need dynamic intervals, use the function form: `autoRefetch: (state) => state.data ? 30_000 : false`. ## Links * API reference: [/api/plugins/auto-refetch](/api/plugins/auto-refetch/src/functions/PiniaColadaAutoRefetch.html) * Source: https://github.com/posva/pinia-colada/tree/main/plugins/auto-refetch --- --- url: /plugins/official/retry.md --- # Retry `@pinia/colada-plugin-retry` retries failed **query** fetches automatically. ## Install ```bash npm i @pinia/colada-plugin-retry ``` ## Enable ```ts import { PiniaColada } from '@pinia/colada' import { PiniaColadaRetry } from '@pinia/colada-plugin-retry' app.use(PiniaColada, { plugins: [ PiniaColadaRetry({ // global defaults retry: 3, // delay can be a number or a function // delay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000), }), ], }) ``` ## Configuration You can configure retry behavior globally (plugin option) and/or per query. * `retry: false` disables retries * `retry: number` retries that many times with no delay * `retry: (failureCount, error) => boolean | number` retries based on the failure count and error. Return `true` to retry immediately, a number to retry after a delay in milliseconds, or `false` to stop retrying. Per-query example: ```ts useQuery({ key: ['todos'], query: fetchTodos, retry: 0, // disable retries for this query }) ``` Or provide a custom policy: ```ts useQuery({ key: ['todos'], query: fetchTodos, retry: (failureCount, error) => { // e.g. retry network errors, but not 4xx return failureCount < 2 && shouldRetry(error) }, }) ``` ## Notes * Retries stop if the query becomes inactive or disabled. * If you trigger multiple fetches quickly, the plugin avoids retrying outdated work. ## Links * API reference: [/api/plugins/retry](/api/plugins/retry/src/functions/PiniaColadaRetry.html) * Source: https://github.com/posva/pinia-colada/tree/main/plugins/retry --- --- url: /plugins/official/delay.md --- # Delay `@pinia/colada-plugin-delay` delays the transition of `asyncStatus` to `'loading'`. This is useful to avoid UI flicker during refreshes (for example when you already have data and are refetching in the background). In other words, if a query is refetching and the refetch takes less than the specified delay, `asyncStatus` will remain `'idle'` and only update to `'loading'` if the delay is exceeded. ## Install ```bash npm i @pinia/colada-plugin-delay ``` ## Enable ```ts import { PiniaColada } from '@pinia/colada' import { PiniaColadaDelay } from '@pinia/colada-plugin-delay' app.use(PiniaColada, { plugins: [ PiniaColadaDelay({ delay: 200, }), ], }) ``` ## Placeholder data When using `placeholderData`, you can keep the previous content while the refresh happens, and only show a loading state if it takes longer than the delay. ## Configuration You can configure the delay globally (plugin option) and/or per query. * `delay: number` (milliseconds) delays setting `asyncStatus` to `'loading'` * `delay: false` disables the behavior You can also override per query: ```ts useQuery({ key: ['todos'], query: fetchTodos, delay: 0, // no delay for this query }) ``` ## Links * API reference: [/api/plugins/delay](/api/plugins/delay/src/functions/PiniaColadaDelay.html) * Source: https://github.com/posva/pinia-colada/tree/main/plugins/delay --- --- url: /plugins/official/cache-persister.md --- # Cache persister `@pinia/colada-plugin-cache-persister` persists the query cache to storage and restores it on startup. ## Install ```bash npm i @pinia/colada-plugin-cache-persister ``` ## Enable ```ts import { PiniaColada } from '@pinia/colada' import { PiniaColadaCachePersister } from '@pinia/colada-plugin-cache-persister' app.use(PiniaColada, { plugins: [ PiniaColadaCachePersister({ // optional key: 'pinia-colada-cache', debounce: 1000, // storage: localStorage, // filter: { key: ['todos'] }, // stringify: JSON.stringify, // parse: JSON.parse, }), ], }) ``` ## Async storage If your storage is asynchronous (e.g. IndexedDB wrappers), you can wait for the cache to be ready before mounting: ```ts import { isCacheReady } from '@pinia/colada-plugin-cache-persister' await isCacheReady() app.mount('#app') ``` ## Configuration You can only configure the plugin globally, not per query: * `key: string` (default: `'pinia-colada-cache'`) is the key used in storage * `storage: Storage` (default: `localStorage`) is the storage to use (must implement `getItem`, `setItem`, and `removeItem`) * `debounce: number` (default: `1000`) is the debounce delay in milliseconds before writing to storage * `filter: { key: QueryKey[] }` (default: `undefined`) is an optional filter to only persist certain queries (by key) * `stringify: (cache) => string` (default: `JSON.stringify`) converts the cache to a string before storing it * `parse: (stored) => cache` (default: `JSON.parse`) restores the cache from the stored string Use `stringify`/`parse` with a codec like [devalue](https://github.com/sveltejs/devalue) to persist `Date`, `Map`, or custom classes. See [Custom Serialization](../../cookbook/cache-persistence.md#Custom-Serialization) for examples. ## Notes * Only successful query results are persisted. * Garbage collection still applies: if an entry is removed, it will disappear from persisted data too. * Serialization, deserialization, and storage failures are ignored because persisted data is treated as a best-effort cache. ## Links * Source: https://github.com/posva/pinia-colada/tree/main/plugins/cache-persister --- --- url: /plugins/community.md --- # Community plugins This page lists third-party plugins built by the community. Want your plugin listed here? * Add it to this page via a pull request * Include a short description, repository link, and npm link * Mention what it extends (queries, mutations, cache persistence, dev UX, …) ::: tip Naming and discoverability Give it a clear name with the `pinia-colada-plugin-` prefix, and add the `pinia-colada-plugin` keyword to your `package.json` to help others find it. ::: Want to build your own plugin? See [Writing plugins](./writing-plugins.md). For integrations that aren't plugins, see [Community](../community.md). ## Recently Successful Adds a `recentlySuccessful` ref to `useMutation()` results. It becomes `true` on success and automatically resets after a configurable duration. * **Extends**: mutations * **Package**: `pinia-colada-plugin-recently-successful` * **Repository**: https://github.com/Barbapapazes/pinia-colada-plugin-recently-successful * **npm**: https://www.npmjs.com/package/pinia-colada-plugin-recently-successful Quick usage: * Register it in `PiniaColada` with `plugins: [PiniaColadaRecentlySuccessfulPlugin()]`. * Use `recentlySuccessful` from `useMutation()`. --- --- url: /plugins/writing-plugins.md --- # Writing Plugins Plugins are install-time functions that subscribe to **Pinia Colada cache actions** allowing you to observe and modify query/mutation lifecycles globally without needing to change your components or the core library. This behavior is based on [Pinia's `$onAction()` subscriptions](https://pinia.vuejs.org/core-concepts/actions.html#Subscribing-to-actions) and is possible because Pinia Colada is built on top of Pinia stores. Under the hood, `useQuery()` and `useMutation()` are composables that trigger actions on their respective caches (`queryCache` and `mutationCache`) to perform their work. Most plugin work falls into one of these categories: * add extra reactive fields to `useQuery()` / `useMutation()` return values (entry **extensions**) * add new options to queries/mutations (via TypeScript module augmentation) * observe outcomes centrally (logging, analytics, notifications) * coordinate timers/retries/refetch policies ::: tip Pinia Colada's core is intentionally minimal. The plugin system is designed to be powerful and flexible enough to cover a wide range of use cases, from simple extensions to complex behaviors like retries or persistence. If you find something is missing, consider implementing it as a plugin! In most cases, you can just point an agent like Claude, Codex, Cursor, ... the codebase of the [delay plugin](https://github.com/posva/pinia-colada/tree/main/plugins/delay) as an example and ask it to generate a new plugin with the desired behavior. ::: ## Conventions Pinia Colada plugins: * should have a clear name with `pinia-colada-plugin-` prefix. * include `pinia-colada-plugin` keyword in package.json. ## Plugin Options Users will install plugins to the project `dependencies` and configure them using the `plugins` array option when registering Pinia Colada: ```ts [./src/main.ts] import { PiniaColada } from '@pinia/colada' import PiniaColadaFeaturePlugin from 'pinia-colada-plugin-feature' app.use(PiniaColada, { plugins: [PiniaColadaFeaturePlugin(options)], }) ``` Plugins are installed in array order. If multiple plugins hook the same cache action, they will run in installation order. ## Plugin Structure Plugins should be factory functions that accept options and return a function that receives the [plugin context](#Plugin-Context). ```ts twoslash import { type PiniaColadaPlugin, useMutationCache } from '@pinia/colada' /** * Options for the Pinia Colada Feature Plugin. */ export interface FeaturePluginOptions { /** * Example option for the plugin. * * @default 'default value' */ foo?: string } const FEATURE_OPTIONS_DEFAULTS = { foo: 'default value', } satisfies Required /** * Example plugin that observes query and mutation lifecycles. * * @param options - plugin options */ export function PiniaColadaFeaturePlugin(options: FeaturePluginOptions = {}): PiniaColadaPlugin { return ({ queryCache, pinia }) => { queryCache.$onAction(({ name, args }) => { if (name === 'setQueryData') { // args type gets narrowed down to the correct type const [queryKey, data] = args } else { // ... } }) // Mutations are tree-shakable, so the mutation cache // is not part of the plugin context. Opt in explicitly. const mutationCache = useMutationCache(pinia) mutationCache.$onAction(({ name, args }) => { if (name === 'mutate') { const [entry] = args } }) } } ``` ## Plugin Context The plugin context is automatically passed to the plugin function and includes: * `queryCache`: the [Query Cache](../advanced/query-cache.md) store (powers `useQuery()` and query utilities) * `pinia`: the Pinia instance (use it to access other stores, like the mutation cache) * `scope`: a Vue `EffectScope` for any reactive work your plugin creates (`ref()`, `watch()`, ...) When using a factory function, the context is passed to the returned function: ```ts import { type PiniaColadaPlugin } from '@pinia/colada' export function PiniaColadaFeaturePlugin(): PiniaColadaPlugin { return ({ queryCache, pinia, scope }) => { // ... } } ``` ## Plugin Subscriptions Pinia Colada plugins rely on [Pinia's action subscriptions](https://pinia.vuejs.org/core-concepts/actions.html#Subscribing-to-actions). When you subscribe to a store with `$onAction()`, Pinia gives you: * `name`: the action name * `args`: the action parameters * `after(cb)`: register a callback that runs when the action completes successfully * `onError(cb)`: register a callback that runs when the action throws/rejects ```ts queryCache.$onAction(({ name, args, after, onError }) => { // before context where you can create shared variables if (name === 'fetch') { const [entry] = args } else if (name === 'setEntryState') { const [entry, state] = args } // this will trigger if the action succeeds and after it has fully run. // it waits for any returned promise after((result) => { console.log('Action succeeded with result:', result) }) // this will trigger if the action throws or returns a promise that rejects onError((error) => { console.error('Action failed with error:', error) }) }) ``` **Observing cache actions** is the core mechanism for plugins. You subscribe to the relevant cache (`queryCache` or `mutationCache`) and you react to the actions that are relevant for your plugin's behavior (e.g. `fetch` for query hooks, `mutate` for mutation hooks, `setEntryState` for any state change, etc.). This works because `useQuery()` and `useMutation()` are composables that **only trigger cache actions** to perform their work. For example, `useQuery()` calls `queryCache.fetch()` to execute the query function and `queryCache.setEntryState()` to update the state. By observing these actions, plugins can hook into the lifecycle of queries and mutations without needing to modify the core logic of `useQuery()` or `useMutation()`. ## Plugins for `useQuery()` `useQuery()` uses the query cache store under the hood, so to hook into `useQuery()`'s lifecycle, subscribe to the `queryCache` actions. Before writing a plugin to augment or modify `useQuery()`'s behavior, it's important to understand its lifecycle: 1. `useQuery()` calls `queryCache.ensure(options, previousEntry?)` to get (or create) an entry. 2. On the first ensure, the store triggers `extend(entry)` **once** so plugins can attach extensions. 3. `queryCache.fetch(entry, options?)` executes the user's `query()` function. 4. `queryCache.setEntryState(entry, state)` is the canonical state-update choke point. 5. `queryCache.remove(entry)` removes an entry (manual or GC). All these steps happen when using `useQuery()` and are observable via the query cache actions. Most query plugins hook into: * `extend(entry)` to define `entry.ext.*` * `fetch(entry, options?)` to observe success/error of the query function * `setEntryState(entry, state)` to observe *any* state change (including `setQueryData()`) * `remove(entry)` to cleanup timers/resources ::: tip `fetch()` rethrows errors. Aborted/outdated requests may not update state, but the action can still reject, your `onError()` handler might see these. ::: ### Cache Keys The cache keys are used to identify queries in the cache. The `key` passed to queries gets serialized deterministically with `toCacheKey()`. This value is saved via `entry.keyHash` and is available in entries too. ```ts twoslash import { toCacheKey, useQueryCache } from '@pinia/colada' const key = toCacheKey(['users', 1, { type: 'friends' }]) const queryCache = useQueryCache() queryCache.getEntries().at(0)?.keyHash ``` ### Query Hooks Pinia Colada intentionally doesn't provide per-component `onSuccess/onError/onSettled` callbacks for queries. For global query callbacks, use `PiniaColadaQueryHooksPlugin`. Internally, it listens to the query cache `fetch` action and wires `after()` / `onError()`. ::: tip This plugin observes **fetches**. If you need "any data change" semantics, hook `setEntryState`. ::: ## Plugins for `useMutation()` `useMutation()` uses the mutation cache store under the hood, so to hook into `useMutation()`'s lifecycle, subscribe to the `mutationCache` actions. ::: info You need to opt in to the mutation cache by calling `useMutationCache(pinia)` in your plugin. This is intentional to tree shake the mutation cache if it's not used in your app. ```ts import { useMutationCache, type PiniaColadaPlugin } from '@pinia/colada' export function PiniaColadaFeaturePlugin(): PiniaColadaPlugin { return ({ pinia }) => { const mutationCache = useMutationCache(pinia) mutationCache.$onAction(({ name, args }) => {}) } } ``` ::: Before writing a plugin to augment or modify `useMutation()`'s behavior, it's important to understand its lifecycle: 1. `useMutation()` creates an initial entry via `mutationCache.create(options)`. 2. The store triggers `extend(entry)` immediately so `useMutation()` can expose extensions right away. 3. Each `mutateAsync(vars)` ensures an entry via `mutationCache.ensure(entry, vars)` and then runs `mutationCache.mutate(entry)`. 4. State updates go through `mutationCache.setEntryState(entry, state)`. Most mutation plugins hook into: * `extend(entry)` to define `entry.ext.*` * `setEntryState(entry, state)` to react to success/error transitions * `remove(entry)` for cleanup ## Plugin Patterns ### Adding options To add new options to queries or mutations, use TypeScript module augmentation to extend `UseQueryOptions` / `UseMutationOptions` and their global counterparts. ```ts import { type PiniaColadaPlugin } from '@pinia/colada' export interface FeaturePluginOptions { myOption?: boolean } export function PiniaColadaFeaturePlugin(options: FeaturePluginOptions = {}): PiniaColadaPlugin { return () => { // plugin implementation } } declare module '@pinia/colada' { interface UseQueryOptions extends FeaturePluginOptions {} interface UseQueryOptionsGlobal extends FeaturePluginOptions {} } ``` Then you can set global defaults for your options when creating Pinia Colada: ```ts [./src/main.ts] import { PiniaColada } from '@pinia/colada' app.use(PiniaColada, { queryOptions: { myOption: true, }, }) ``` For a detailed TypeScript example, see the [Typescript section](#TypeScript) below. ### Adding entry extensions Entry extensions are properties stored on `entry.ext` that become available on the objects returned by `useQuery()` and `useMutation()`. ```ts import { type PiniaColadaPlugin } from '@pinia/colada' export function PiniaColadaFeaturePlugin(): PiniaColadaPlugin { return ({ queryCache, scope }) => { queryCache.$onAction(({ name, args }) => { if (name === 'extend') { const [entry] = args // When defining reactive extensions, make sure to create them inside `scope.run()` so they are properly disposed when the entry is removed. scope.run(() => { entry.ext.myField = shallowRef('initial value') // This will be reactive and available on the query return value }) } }) } } declare module '@pinia/colada' { interface UseQueryEntryExtensions { /** * Example of an extension field added by the plugin. */ myField: ShallowRef } } ``` ```vue ``` Of course, this is also possible for mutations by hooking the mutation cache and augmenting `UseMutationEntryExtensions`. ```ts import { useMutationCache, type PiniaColadaPlugin } from '@pinia/colada' export function PiniaColadaFeaturePlugin(): PiniaColadaPlugin { return ({ pinia, scope }) => { const mutationCache = useMutationCache(pinia) mutationCache.$onAction(({ name, args }) => { if (name === 'extend') { const [entry] = args // When defining reactive extensions, make sure to create them inside `scope.run()` so they are properly disposed when the entry is removed. scope.run(() => { entry.ext.myField = shallowRef('initial value') // This will be reactive and available on the mutation return value }) } }) } } declare module '@pinia/colada' { interface UseMutationEntryExtensions { /** * Example of an extension field added by the plugin. */ myField: ShallowRef } } ``` ```vue ``` You should only add new keys to `entry.ext` during the `extend` action as it is shared across all plugins and it's called only once per entry. Do not assign it a completely new object. Also, you can't add new keys to `entry.ext` later (e.g. in `fetch` or `setEntryState`). You must define all the needed keys in `extend` so they are available on the return value from the start. ### Examples Add `dataUpdatedAt` to queries: ```ts twoslash import type { PiniaColadaPlugin } from '@pinia/colada' import { type ShallowRef, shallowRef } from 'vue' /** * Adds a `dataUpdatedAt` property to queries that represents the last time the * data was updated. */ export function PiniaColadaDataUpdatedAtPlugin(): PiniaColadaPlugin { return ({ queryCache, scope }) => { queryCache.$onAction(({ name, args, after }) => { // Use the `extend` action to add custom properties if (name === 'extend') { const [entry] = args // All effects must be created within the scope scope.run(() => { entry.ext.dataUpdatedAt = shallowRef(entry.when) }) } else if (name === 'setEntryState') { const [entry] = args after(() => { entry.ext.dataUpdatedAt.value = entry.when }) } }) } } declare module '@pinia/colada' { interface UseQueryEntryExtensions { /** * Time stamp of the last time the data was updated. */ dataUpdatedAt: ShallowRef } } ``` Then you can use `dataUpdatedAt` in your components: ```vue ``` Another example, add `mutatedAt` to mutations: ```ts twoslash import { type PiniaColadaPlugin, useMutationCache } from '@pinia/colada' import { type ShallowRef, shallowRef } from 'vue' export function PiniaColadaMutatedAtPlugin(): PiniaColadaPlugin { return ({ pinia, scope }) => { const mutationCache = useMutationCache(pinia) mutationCache.$onAction(({ name, args, after }) => { if (name === 'extend') { const [entry] = args scope.run(() => { entry.ext.mutatedAt = shallowRef(0) }) } else if (name === 'setEntryState') { const [entry, state] = args after(() => { if (state.status === 'success') { entry.ext.mutatedAt.value = entry.when } }) } }) } } declare module '@pinia/colada' { interface UseMutationEntryExtensions { /** * Timestamp of the last successful mutation. */ mutatedAt: ShallowRef } } ``` Then you can use `mutatedAt` in your components: ```vue ``` ## TypeScript Plugins often need to augment types (options and/or entry extensions). This is done with module augmentation. ### Adding options to queries Augment both `UseQueryOptions` (generic) and `UseQueryOptionsGlobal` (global defaults): ```ts export interface MyQueryPluginOptions { myOption?: boolean } declare module '@pinia/colada' { interface UseQueryOptions extends MyQueryPluginOptions {} interface UseQueryOptionsGlobal extends MyQueryPluginOptions {} } ``` ::: info You must keep all the generic parameters (`TData`, `TError`, `TDataInitial`) even if you don't use them, otherwise you'll get type errors. ::: Global options are set when creating Pinia Colada: ```ts import { PiniaColada } from '@pinia/colada' app.use(PiniaColada, { queryOptions: { myOption: true, }, }) ``` ### Adding extensions to queries Augment `UseQueryEntryExtensions`: ```ts declare module '@pinia/colada' { interface UseQueryEntryExtensions { myField: string } } ``` ::: warning You must keep all the generic parameters (`TData`, `TError`) even if you don't use them, otherwise you'll get type errors. ::: Then you can set `entry.ext.myField` in the plugin and it will be available on the query return value. ```vue ``` ### Adding options to mutations Augment both `UseMutationOptions` (generic) and `UseMutationOptionsGlobal`: ```ts export interface MyMutationPluginOptions { myOption?: boolean } declare module '@pinia/colada' { interface UseMutationOptions extends MyMutationPluginOptions {} interface UseMutationOptionsGlobal extends MyMutationPluginOptions {} } ``` ::: info You must keep all the generic parameters (`TData`, `TVars`, `TError`, `TContext`) even if you don't use them, otherwise you'll get type errors. ::: Global options are set when creating Pinia Colada: ```ts [./src/main.ts] import { PiniaColada } from '@pinia/colada' app.use(PiniaColada, { mutationOptions: { myOption: true, }, }) ``` ### Adding extensions to mutations Augment `UseMutationEntryExtensions`: ```ts declare module '@pinia/colada' { interface UseMutationEntryExtensions { myField: string } } ``` ::: warning You must keep all the generic parameters (`TData`, `TVars`, `TError`, `TContext`) even if you don't use them, otherwise you'll get type errors. ::: Then you can set `entry.ext.myField` in the plugin and it will be available on the mutation return value. ```vue ``` --- --- url: /advanced/query-cache.md --- # Query Cache The query cache is a central piece of the Pinia colada library. It centralizes the caching logic of queries and is used for [query invalidation](/guide/query-invalidation.md) and [optimistic updates](/guide/optimistic-updates.md). It's implemented as a Pinia store. It can be accessed in components setup and other *injectable contexts* (e.g. Pinia stores, Router navigation guards) with `useQueryCache()`. ```vue twoslash ``` ## Two flavors of actions The cache exposes two kinds of actions, intentionally: * **Precise actions** take an **entry** and do exactly one thing. They are minimal and composable, letting you build the exact behavior you need: `cancel(entry)`, `remove(entry)`, `invalidate(entry)`, `fetch(entry)`, `refresh(entry)`, `setEntryState(entry, state)`. * **Convenience actions** take a **key** or **filters** and batch over matching entries. They cover the common cases without forcing you to look up entries first: `invalidateQueries(filters)`, `cancelQueries(filters)`, `setQueriesData(filters, updater)`, `getQueryData(key)`, `setQueryData(key, data)`. To go from a key or filters to entries (so you can call precise actions), use `get(key)` and `getEntries(filters)`. `ensure(options)` also returns an entry, useful when you want to interact with a query that might not exist yet. As a rule of thumb, reach for a `*Queries` action first; drop down to precise actions when you need control the convenience version doesn't give you. ## Reading the cache: `get()` and `getEntries()` These are the foundations. They don't mutate and they don't subscribe; they return a snapshot of the cache at the time of the call. You can use them anywhere `useQueryCache()` is available. * `get(key)` returns the entry for that key, or `undefined`. Use it when you have an exact key. * `getEntries(filters?)` returns every entry matching the filters. With no argument, it returns every entry in the cache. Filters accept the following options: * `key`: a key prefix. Matches the key and its children. * `exact: true`: restrict to the exact key, excluding children. Requires `key`. * `active: true | false | null`: only entries currently in use by a component (or not). * `stale: true | false | null`: only stale (or fresh) entries. * `status: 'pending' | 'success' | 'error' | null`: only entries with the given status. * `predicate: (entry) => boolean`: custom logic, runs after the other filters match. ```ts // every stale entry under ['todos'] const stale = queryCache.getEntries({ key: ['todos'], stale: true }) ``` ## Combining actions Some workflows have no single convenience method because they combine concerns intentionally. The most common one is **clearing the cache**: `remove()` doesn't cancel pending requests, so a pending fetch can still resolve and rewrite state after you remove the entry. A full clear is two steps: ```ts // abort pending requests first, then drop the entries queryCache.cancelQueries() queryCache.getEntries().forEach((entry) => queryCache.remove(entry)) ``` Components currently using a removed entry will recreate it on their next read. This is a reset, not a freeze. The same shape works for **resetting a single query** (e.g. on logout): ```ts const entry = queryCache.get(['user', userId]) if (entry) { queryCache.cancel(entry) queryCache.remove(entry) } ``` ## Common patterns with `*Queries` actions For everything else, the convenience actions take a key or filters directly: * **`invalidateQueries(filters?, refetchActive?)`**: mark matching entries stale and refetch the active ones. The everyday tool after a mutation. See [Query Invalidation](/guide/query-invalidation.md) for filter behavior and the `'all'` option. * **`cancelQueries(filters?, reason?)`**: abort pending fetches for matching entries. Used in [Optimistic Updates](/guide/optimistic-updates.md) to stop in-flight fetches from overwriting the optimistic state. * **`setQueriesData(filters, updater)`**: bulk-patch matching entries with an updater function. Use it when one server-side change should be reflected across many cached queries (e.g. a contact appearing in several lists). Like `setQueryData`, it forces `status: 'success'` on every match, including entries that never loaded, so filter by `status: 'success'` to only patch loaded data. * **`getQueryData(key)` / `setQueryData(key, data | updater)`**: direct read/write of a single entry's data by key. `setQueryData` creates the entry if missing and forces `status: 'success'`. ```ts // after creating a contact, patch every loaded list it appears in queryCache.setQueriesData( { key: ['contacts', 'list'], status: 'success' }, (list = []) => [...list, newContact], ) ``` ## See also * [Query Invalidation](/guide/query-invalidation.md) * [Optimistic Updates](/guide/optimistic-updates.md) * [Cancelling Queries](/guide/cancelling-queries.md) --- --- url: /advanced/reusable-queries.md --- # Reusable Queries While `useQuery()` can be directly called in components, we often need to reuse the same query across components or even add extra properties like consuming the route via `useRoute()` or passing a *search* text to the API request. In those scenarios, it's more convenient to *define* queries and reuse them where needed. But why is this necessary? Can't we just create a regular composable for this? ```ts twoslash // src/queries/todos.ts import { useQuery } from '@pinia/colada' import { ref } from 'vue' export function useFilteredTodos() { const search = ref('') const query = useQuery({ key: () => ['todos', search.value], query: () => fetch(`/api/todos?search=${search.value}`).then((res) => res.json()), }) return { search, ...query } } ``` *Not exactly* because we are mixing component state (`search`) with global state (`useQuery()` creates global state). There are two main issues with this approach: * The ref `search` isn't shared among components. Each component instance creates a new ref for themselves * If the query is reused across different components, there will be a de-synchronization between the `search` ref instantiated in each component and the one used in the query's key. Since the query is global, it's only instantiated once, and only the first `search` is used. Therefore, if we use this approach, only the changes to the `search` **of the component that first instantiated the query will take effect**. Because of this, Pinia Colada provides an alternative way of defining a query, through the `defineQuery` composable. Simply wrap your composable with it: ::: code-group ```ts [src/queries/todos.ts] twoslash import { defineQuery, useQuery } from '@pinia/colada' import { ref } from 'vue' export const useFilteredTodos = defineQuery(() => { // [!code ++] // `search` is shared by all components using this query const search = ref('') const { state, ...rest } = useQuery({ key: () => ['todos', { search: search.value }], query: () => fetch(`/api/todos?filter=${search.value}`, { method: 'GET' }), }) return { ...rest, // we can rename properties for convenience too todoList: state, search, } }) // [!code ++] ``` ```vue [src/pages/todo-list.vue] ``` ::: Consider `useFilteredTodos()` as a globally shared composable, instantiated only once. This ensures the `search` ref is shared across all components using this query, reflecting changes universally across components. ::: tip When to use `defineQuery()` over just `useQuery()`? If you find yourself with the same query (same `key` value) used in multiple components that are also mounted at the same time, you **should** use `defineQuery()`. This will ensure that the query is shared among all components. If you need to reuse a query in multiple components, move the query to a separate file (e.g. `src/queries/todos.ts`) and [use `defineQueryOptions()`](../guide/queries.md#Organizing-Queries) to define reusable options. This will ensure that the query code isn't partially updated in your code base. If you need to define custom parameters **that aren't global**, [prefer using `defineQueryOptions`](../guide/queries.md#Organizing-Queries). You can also or write a custom composable and call `useQuery()` within it. ::: ### Nuxt When using `defineQuery()` in Nuxt, `useRoute()` returns a different version of the route, **it is recommended to explicitly import it from `vue-router` instead of using the Nuxt version** (automatically imported): ```ts{1} import { useRoute } from 'vue-router' export const useContactDetails = defineQuery(() => { const route = useRoute() return useQuery({ key: () => ['contacts', route.params.contactId], query: () => fetch(`/api/contacts/${route.params.contactId}`).then((res) => res.json()), }) }) ``` If you don't do this, you will see the query being triggered more than it should, specifically when navigating to or away from the page. It might be even `undefined`. This is due to how Nuxt internally handles the integration with Suspense. ## Caveat: SSR and `defineQuery()` While `defineQuery()` looks like a [setup store](https://pinia.vuejs.org/core-concepts/#Setup-Stores) in pinia, it doesn't define a store, **the state returned is not serialized to the page**. This means that you are fully responsible for ensuring consistent values across the server and client for anything that is not returned by `useQuery()`. In short, this means that you cannot have code like this: ```ts defineQuery(() => { const search = ref('') // ❌ different values on client and server if (import.meta.env.SSR) { search.value = fetchSomeInitialValue() } const query = useQuery({ key: () => ['todos', search.value], query: () => fetch(`/api/todos?search=${search.value}`).then((res) => res.json()), }) return { search, ...query, } }) ``` Instead, you will need to ensure the `search` state is serialized from the server to the client. The simplest way is to move it to a store: ```ts twoslash import { defineStore, storeToRefs } from 'pinia' // ---cut-start--- import 'vite/client' import { defineQuery, useQuery } from '@pinia/colada' import { ref } from 'vue' function getInitialValue() { return 'initial value' } // ---cut-end--- // @moduleResolution: bundler const useLocalStore = defineStore('query-search-store', () => { const search = ref('') return { search } }) defineQuery(() => { const { search } = storeToRefs(useLocalStore()) if (import.meta.env.SSR) { search.value = getInitialValue() } const query = useQuery({ key: () => ['todos', search.value], query: () => fetch(`/api/todos?search=${search.value}`).then((res) => res.json()), }) return { search, ...query, } }) ``` ::: tip If you are using Nuxt, you can simply replace the `ref()` with [Nuxt's `useState()`](https://nuxt.com/docs/api/composables/use-state) instead of creating a store. ::: --- --- url: /errors.md --- # Errors and Warnings Reference Pinia Colada reports user-facing errors and warnings with stable diagnostic codes. Each code links to a page explaining what happened, how to fix it, and common causes. Codes are permanent: they are never renamed or reused. Codes follow the pattern `PC_XNNNN` where the letter indicates the area: * `R`: runtime * `C`: configuration * `B`: build * `D`: deprecation Most diagnostics are only reported during development and are removed from production builds. ## Runtime | Code | Summary | | -------------------------------- | ------------------------------------------------------------------------ | | [PINIA\_COLADA\_R0001](./pinia_colada_r0001.md) | `useQueryCache()`/`useMutationCache()` called outside injection context | | [PINIA\_COLADA\_R0002](./pinia_colada_r0002.md) | The query or mutation cache was directly set | | [PINIA\_COLADA\_R0003](./pinia_colada_r0003.md) | `useQuery()` called with an empty array as the key | | [PINIA\_COLADA\_R0004](./pinia_colada_r0004.md) | `entry.refresh()`/`entry.fetch()` called on an entry without options | | [PINIA\_COLADA\_R0005](./pinia_colada_r0005.md) | A mutation entry was mutated before being ensured | | [PINIA\_COLADA\_R0006](./pinia_colada_r0006.md) | A mutation entry was reused | | [PINIA\_COLADA\_R0007](./pinia_colada_r0007.md) | `defineMutation()` called outside of a component or effect scope | | [PINIA\_COLADA\_R0008](./pinia_colada_r0008.md) | Loading a previous page without `getPreviousPageParam` | | [PINIA\_COLADA\_R0009](./pinia_colada_r0009.md) | Cannot load more pages: query entry not found in cache | | [PINIA\_COLADA\_R0010](./pinia_colada_r0010.md) | An active query entry was removed from the cache | ## Configuration | Code | Summary | | -------------------------------- | ------------------------------------------------------------------------ | | [PINIA\_COLADA\_C0001](./pinia_colada_c0001.md) | Root pinia plugin not detected when installing `PiniaColada` | --- --- url: /errors/pinia_colada_c0001.md --- # PINIA\_COLADA\_C0001: root pinia plugin not detected * Level: error (thrown, dev only) ## What happened The `PiniaColada` plugin was installed but no Pinia instance could be found on the application. Pinia Colada relies on Pinia stores, so it cannot work without an active Pinia instance. ## How to fix it Install Pinia before installing the `PiniaColada` plugin: ```ts import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' const app = createApp(App) app.use(createPinia()) // must come first app.use(PiniaColada) ``` ## Passing the Pinia instance explicitly When you already have a Pinia instance at hand, pass it through the plugin options instead of relying on auto-detection: ```ts const pinia = createPinia() app.use(PiniaColada, { pinia }) ``` `useQueryCache()` and `useMutationCache()` are Pinia stores, so they accept the same explicit instance when called outside of an injection context: ```ts const queryCache = useQueryCache(pinia) ``` ## In a Nuxt plugin A Nuxt plugin commonly registers callbacks (hooks, watchers, navigation guards) that run later. Composables that rely on injection β€” including `useQueryCache()` β€” must be called synchronously in the plugin's main context, exactly like any other composable, and the resolved instance used inside the callback. Calling them lazily from within the callback loses the injection context (and may pick up the wrong Pinia), triggering [PINIA\_COLADA\_R0001](./pinia_colada_r0001.md). ```ts // plugins/refresh-on-auth.ts export default defineNuxtPlugin(() => { // βœ… hoist the composable to the plugin's main context const queryCache = useQueryCache() const auth = useAuthStore() watch( () => auth.user, () => { // βœ… use the already-resolved instance inside the callback queryCache.invalidateQueries({ key: ['todos'] }) }, ) }) ``` ```ts export default defineNuxtPlugin(() => { const auth = useAuthStore() watch( () => auth.user, () => { // ❌ no injection context here β†’ PINIA_COLADA_R0001, and the wrong Pinia may be used const queryCache = useQueryCache() queryCache.invalidateQueries({ key: ['todos'] }) }, ) }) ``` ## Common causes * Calling `app.use(PiniaColada)` before `app.use(createPinia())` * Forgetting to install Pinia altogether * Using multiple apps and installing Pinia Colada on an app without Pinia --- --- url: /errors/pinia_colada_r0001.md --- # PINIA\_COLADA\_R0001: cache composable called outside of an injection context * Level: warning (dev only, reported once) ## What happened `useQueryCache()` or `useMutationCache()` was called outside of an injection context (component `setup()`, store, navigation guard). These composables rely on Vue's `inject()` under the hood, so Vue will also emit its own warning about `inject` being used incorrectly, and the wrong app context might be picked up. ## How to fix it Only call cache composables in places where injection is available: ```ts // βœ… inside a component setup const queryCache = useQueryCache() // βœ… inside a store export const useTodosStore = defineStore('todos', () => { const queryCache = useQueryCache() // ... }) // βœ… inside a navigation guard router.beforeEach(() => { const queryCache = useQueryCache() // ... }) ``` If you need it elsewhere (e.g. outside of the app), pass the pinia instance explicitly: `useQueryCache(pinia)`. See [Usage restrictions for composables](https://vuejs.org/guide/reusability/composables.html#usage-restrictions). ## Common causes * Calling `useQueryCache()` at the top level of a module * Calling it inside an event handler or `setTimeout` callback instead of `setup()` * Calling it in a plain function that is not itself called from an injection context --- --- url: /errors/pinia_colada_r0002.md --- # PINIA\_COLADA\_R0002: the cache cannot be directly set * Level: error (logged, dev only) ## What happened The query cache or the mutation cache was replaced by assigning to it (e.g. `queryCache.caches = ...`). The caches are internal reactive structures that must be modified through the cache methods, never swapped out. **This will fail in production** where the development check (and its error message) is removed. ## How to fix it Use the cache methods to modify entries instead of replacing the cache: ```ts const queryCache = useQueryCache() // βœ… modify data queryCache.setQueryData(['todos'], newTodos) // βœ… invalidate entries queryCache.invalidateQueries({ key: ['todos'] }) // ❌ never replace the cache itself queryCache.caches = new Map() ``` ### Deleting entries completely If you actually want to *remove* entries from the cache (not just invalidate them), use `remove()` on the entries instead of clearing the cache. `remove()` takes a single entry, so combine it with `getEntries()` to delete one or many: ```ts const queryCache = useQueryCache() // remove a single entry by key (if it exists) const entry = queryCache.get(['todos']) if (entry) queryCache.remove(entry) // remove every matching entry (omit the filter to clear everything) // `active: false` skips entries still used by a mounted `useQuery()`, // which would otherwise be recreated and refetched right away for (const entry of queryCache.getEntries({ key: ['todos'], active: false })) { queryCache.remove(entry) } ``` Keep these pitfalls in mind: * `remove()` clears the entry's garbage-collection timeout and deletes it from the cache, but it does **not** abort an in-flight request. Call `queryCache.cancel(entry)` (or `cancelQueries(filters)`) first if a fetch might still be pending. * Removing an entry that is still **active** (used by a mounted `useQuery()`) will cause it to be recreated and refetched on the next access, so removing rarely makes sense for active entries β€” prefer `invalidateQueries()` there. * `getEntries()` with no filter returns every entry, and `getEntries({ key: ['todos'] })` matches the key *and all its children*. Pass `exact: true` to match only the exact key. ## Common causes * Trying to "reset" the cache by assigning a new object to it * Spreading or cloning the cache store and writing it back * State management devtools or plugins replacing store state wholesale (e.g. naive store `$patch`/hydration logic) --- --- url: /errors/pinia_colada_r0003.md --- # PINIA\_COLADA\_R0003: useQuery() was called with an empty key * Level: error (thrown, dev only) ## What happened `useQuery()` (or another query composable) was called with an empty array as the `key`. Keys identify entries in the cache, so they must contain at least one element. ## How to fix it Pass a key with at least one element: ```ts // ❌ empty key useQuery({ key: [], query: fetchTodos }) // βœ… useQuery({ key: ['todos'], query: fetchTodos }) ``` ## Common causes * Computing the key from a list that happens to be empty * A `key` getter returning `[]` while waiting for some data; return a placeholder segment instead (e.g. `['todo', id ?? 'none']`) or guard with `enabled` --- --- url: /errors/pinia_colada_r0004.md --- # PINIA\_COLADA\_R0004: entry has no options * Level: error (thrown, dev only) ## What happened `queryCache.refresh(entry)` or `queryCache.fetch(entry)` was called with an entry that has no options. Entries only receive options once they are used by `useQuery()` (or ensured with options); entries created indirectly (e.g. by `setQueryData()` on a key that was never used by a query) cannot be fetched because there is no `query` function associated with them. ## How to fix it Only refresh or fetch entries that have been initialized by a query: ```ts const queryCache = useQueryCache() const entry = queryCache.getEntries({ key: ['todos'], exact: true }).at(0) // only fetch entries that have options if (entry?.options) { await queryCache.refresh(entry) } ``` If the entry was expected to have options, this is probably a bug in Pinia Colada: please open an issue with a boiled down reproduction. ## Common causes * Calling `queryCache.fetch()`/`queryCache.refresh()` on an entry created only via `setQueryData()` or hydration * Iterating over all entries with `getEntries()` and fetching them without filtering out option-less entries --- --- url: /errors/pinia_colada_r0005.md --- # PINIA\_COLADA\_R0005: mutation entry mutated before being ensured * Level: error (logged, dev only) ## What happened `mutationCache.mutate()` was called with a mutation entry that was never *ensured*. Ensuring an entry registers it in the cache and assigns it an id; mutating an entry that skipped this step leaves the cache in an inconsistent state. ## How to fix it Always ensure the entry first when calling `mutationCache.mutate()` manually. `ensure()` takes the entry returned by `create()` plus the variables: ```ts const mutationCache = useMutationCache() const entry = mutationCache.create(options) // βœ… ensure first, then mutate mutationCache.mutate(mutationCache.ensure(entry, vars)) // ❌ mutating an entry that was only created, never ensured mutationCache.mutate(entry) ``` If you are not calling `mutationCache.mutate()` manually, this is probably a bug in Pinia Colada: please open an issue on GitHub with a boiled down reproduction. ## Common causes * Calling `mutationCache.mutate()` with an entry obtained from `create()` instead of `ensure()` * Reusing internal cache APIs in plugins without going through `ensure()` --- --- url: /errors/pinia_colada_r0006.md --- # PINIA\_COLADA\_R0006: mutation entry reused * Level: error (logged, dev only) ## What happened `mutationCache.mutate()` was called with a mutation entry that already settled or that has an ongoing request. Each call of a mutation must use a fresh entry: reusing one overwrites its state and can mix up concurrent invocations. ## How to fix it Re-ensure before every mutation call. `ensure()` takes the entry from `create()` plus the variables, and returns a fresh entry when the one you pass was already used: ```ts const mutationCache = useMutationCache() let entry = mutationCache.create(options) // βœ… re-ensure before each call to get a fresh entry entry = mutationCache.ensure(entry, vars) mutationCache.mutate(entry) entry = mutationCache.ensure(entry, vars) mutationCache.mutate(entry) // ❌ reusing the same ensured entry for a second call entry = mutationCache.ensure(entry, vars) mutationCache.mutate(entry) mutationCache.mutate(entry) ``` If you are not calling `mutationCache.mutate()` manually, this is probably a bug in Pinia Colada: please open an issue on GitHub with a boiled down reproduction. ## Common causes * Caching a mutation entry and mutating it multiple times * Calling `mutationCache.mutate()` again before the previous call finished with the same entry --- --- url: /errors/pinia_colada_r0007.md --- # PINIA\_COLADA\_R0007: defineMutation() called outside of a component or effect scope * Level: warning (dev only) ## What happened A composable returned by `defineMutation()` was called outside of a component and outside of an effect scope. The mutation's reactive effects are attached to the current scope so they can be paused when no consumer is left; without a scope, they will never be cleaned up, which may cause memory leaks. ## How to fix it Call the composable inside a component `setup()`, an effect scope, or a store: ```ts const useCreateTodo = defineMutation({ key: ['todos', 'create'], mutation: (text: string) => createTodo(text), }) // βœ… inside a component setup const { mutate } = useCreateTodo() // βœ… inside an effect scope const scope = effectScope() scope.run(() => useCreateTodo()) // ❌ at the top level of a module or inside an event handler ``` ## Common causes * Calling the defined mutation at the top level of a module * Calling it inside a plain event handler or async callback that runs after `setup()` finished --- --- url: /errors/pinia_colada_r0008.md --- # PINIA\_COLADA\_R0008: missing getPreviousPageParam * Level: error (thrown, dev only) ## What happened `loadPreviousPage()` was called on an infinite query that does not define `getPreviousPageParam` in its options. Without it, Pinia Colada cannot compute the parameter of the previous page. **This will fail in production** where the development check is removed. ## How to fix it Add a `getPreviousPageParam()` function to the `useInfiniteQuery()` options: ```ts useInfiniteQuery({ key: ['messages'], query: ({ pageParam }) => fetchMessages({ cursor: pageParam }), initialPageParam: null as string | null, getNextPageParam: (lastPage) => lastPage.nextCursor, getPreviousPageParam: (firstPage) => firstPage.previousCursor, // [!code ++] }) ``` ## Common causes * Calling `loadPreviousPage()` on a query that only paginates forward * Forgetting `getPreviousPageParam` when adding bidirectional pagination --- --- url: /errors/pinia_colada_r0009.md --- # PINIA\_COLADA\_R0009: query entry not found in cache * Level: warning (dev only) ## What happened `loadNextPage()` or `loadPreviousPage()` was called but the corresponding query entry could not be found in the cache. The call is ignored and resolves to `null`. This usually means the infinite query has not run yet or its entry was removed from the cache. ## How to fix it Make sure the infinite query is active (e.g. used by a mounted component) before loading more pages: ```ts const { state, loadNextPage } = useInfiniteQuery({ key: ['items'], query: ({ pageParam }) => fetchItems(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextPage, }) // ❌ the entry could be removed from the cache before this runs onUnmounted(() => { setTimeout(() => { loadNextPage() }, 1000) }) ``` ## Common causes * The entry was garbage collected (e.g. after `gcTime`) or removed from the cache while a component still holds a reference to `loadNextPage()` * A dynamic `key` that changed, so the entry for the new key does not exist yet (race condition) --- --- url: /cookbook.md --- # Cookbook * [Prefetching data](./prefetching.md) * [Persisting the cache](./cache-persistence.md) * [Query Hooks](./query-hooks.md) * [Testing](./testing.md) * [Migrating from `@tanstack/vue-query` to `@pinia/colada`](./migration-tvq.md) * [Migration Codemods](./migrations.md) * [Migrating Devtools to v2](./migration-devtools-v2.md) --- --- url: /cookbook/prefetching.md --- # Prefetching queries Prefetching queries is a way to ensure that your data is ready before `useQuery()` is called. Usually done before navigating to a page or to prefetch results in paginated results. You have multiple ways to prefetch queries. ## Thanks to defineQueryOptions When options do not contain dynamic keys, they can be easily prefetched by *ensuring* and *refreshing* an entry: ```ts import { defineQueryOptions, useQueryCache } from '@pinia/colada' const userListQuery = defineQueryOptions({ key: ['users', 'list'], query: () => fetch('/api/users').then((res) => res.json()), }) const queryCache = useQueryCache() await queryCache.refresh(queryCache.ensure(userListQuery)) ``` It's up to you to await it or not. If a component that uses this query is mounted after the prefetch, it will **reuse the cached data or the ongoing request**. Using `defineQueryOptions` is the recommended way to define queries that you want to prefetch, as it allows you to easily reference queries and **is type-safe**. ## Setting the data directly You can also set the data directly in the cache. This is useful when you get the data from another source rather than making a request: ```ts import { useQueryCache } from '@pinia/colada' const queryCache = useQueryCache() queryCache.setQueryData( ['users', 'list'], // don't forget the await! await fetch('/api/users').then((res) => res.json()), ) ``` Differently from using `ensure()` or `useQuery()`, this does not attach any query options to the cache entry, so it won't be able to refetch or know when the data is stale. In most cases this is fine, but if you want to have a fully functional query, it's better to use `ensure()` first to attach options to the *query entry*. We can combine this with [`defineQueryOptions`](/guide/queries.md#Organizing-Queries) to have type safety and a consistent way to reference queries: ```ts import { defineQueryOptions } from '@pinia/colada' import { fetchItemById } from '@/api/items' import type { Item } from '@/api/items' export const itemDetailQuery = defineQueryOptions((id: number) => ({ key: ['items', id], query: () => fetchItemById(id), })) ``` Then you can ensure the entry and set the data: ```ts import { useQueryCache } from '@pinia/colada' import { itemDetailQuery } from './queries/items' const queryCache = useQueryCache() const queryOptions = itemDetailQuery(item.id) queryCache.ensure(queryOptions) queryCache.setQueryData(queryOptions.key, itemData) ``` --- --- url: /cookbook/cache-persistence.md --- # Cache Persistence Persist your query cache to storage so users don't start with empty state on page reload. ::: tip GC Time Queries are removed from storage when they are garbage collected. Increase `gcTime` to keep data longer: ```ts useQuery({ key: ['users'], query: fetchUsers, gcTime: 1000 * 60 * 60 * 24, // 24 hours }) ``` ::: ## Setup Install the plugin: ```sh npm install @pinia/colada-plugin-cache-persister ``` Add it to your app: ```ts import { PiniaColadaCachePersister } from '@pinia/colada-plugin-cache-persister' app.use(PiniaColada, { plugins: [PiniaColadaCachePersister()], }) ``` ## Filtering Queries By default, all queries are persisted. Use the `filter` option to persist only specific queries: ```ts // Persist queries starting with 'users' PiniaColadaCachePersister({ filter: { key: ['users'] }, }) ``` Or use a predicate function for more control: ```ts PiniaColadaCachePersister({ filter: { predicate: (entry) => !entry.key.includes('sensitive'), }, }) ``` ## Custom Serialization The plugin stores the cache with `JSON.stringify` and restores it with `JSON.parse`. JSON cannot represent values like `Date`, `Map`, or `Set`, so they come back as plain strings or objects after a reload. The `stringify` and `parse` options swap in a codec that preserves them, like [devalue](https://github.com/sveltejs/devalue), which you can pass as-is: ```ts import { parse, stringify } from 'devalue' PiniaColadaCachePersister({ stringify, parse, }) ``` `devalue` handles `Date`, `Map`, `Set`, and friends out of the box. For your own classes, register a reducer and the matching reviver: ```ts import { parse, stringify } from 'devalue' PiniaColadaCachePersister({ stringify: (cache) => stringify(cache, { Money: (v) => v instanceof Money && [v.amount, v.currency] }), parse: (stored) => parse(stored, { Money: ([amount, currency]) => new Money(amount, currency) }), }) ``` The object handed to `stringify`, and expected back from `parse`, maps each query key hash to a `[data, error, when, meta]` tuple, where `when` is the local time (`Date.now()`) of its last fetch. Because that timestamp is absolute, an entry restored days later is correctly stale and refetches on mount instead of being served as fresh. Persistence is best-effort: if serializing or restoring the cache throws, the plugin skips that write or read and continues with a stale or empty cache instead of crashing. ## Custom Storage The plugin works with any storage that implements `getItem` and `setItem`. This includes async storage like IndexedDB: ```ts import { get, set } from 'idb-keyval' PiniaColadaCachePersister({ storage: { getItem: (key) => get(key), setItem: (key, value) => set(key, value), }, }) ``` If you use an async storage, ensure the cache is restored before your app mounts: ```ts import { PiniaColadaCachePersister, isCacheReady } from '@pinia/colada-plugin-cache-persister' async function main() { const app = createApp(App) app.use(pinia) app.use(PiniaColada, { plugins: [PiniaColadaCachePersister()], }) await isCacheReady() app.mount('#app') } main() ``` ## Options | Option | Type | Default | Description | | ------------- | ------------------------------- | ---------------------- | ------------------------------------------------------ | | `key` | `string` | `'pinia-colada-cache'` | Storage key used to persist the cache | | `storage` | `Storage \| PiniaColadaStorage` | `localStorage` | Storage backend (sync or async) | | `filter` | `UseQueryEntryFilter` | - | Filter which queries to persist | | `debounce` | `number` | `1000` | Debounce time in ms before persisting | | `stringify` | `(cache) => string` | `JSON.stringify` | Convert the persisted cache object to a string | | `parse` | `(stored) => cache` | `JSON.parse` | Restore the persisted cache object from a string | --- --- url: /cookbook/testing.md --- # Testing When testing components that use Pinia Colada, focus on **testing your app behavior**, not Pinia Colada internals. In most cases, mock at the network layer so queries and mutations run through the real Pinia Colada flow. ## Avoid `createTestingPinia` ::: danger Do **not** use `createTestingPinia()` from `@pinia/testing`. It stubs Pinia actions, which breaks Pinia Colada's internal stores. Always use a plain `createPinia()` in your tests. ::: ## Setup Install test dependencies: ```bash npm i -D msw vitest @vue/test-utils ``` Set up MSW for your test runner before following the examples: * [MSW Getting Started](https://mswjs.io/docs/getting-started) * [MSW Node integration](https://mswjs.io/docs/integrations/node) ### Test Boilerplate Mount components with `createPinia()` and `PiniaColada`: ```ts import { describe, it, expect } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' function mountWithPlugins(component, options = {}) { return mount(component, { global: { plugins: [createPinia(), PiniaColada], }, ...options, }) } ``` ::: tip Use `happy-dom` or `jsdom` as your [Vitest environment](https://vitest.dev/config/#environment) since components need a DOM. ::: ## Testing Queries Example component: ```vue ``` Test it by intercepting the request with MSW: ```ts import { http, HttpResponse } from 'msw' import { server } from './mocks/server' import ContactList from './ContactList.vue' it('renders contacts', async () => { server.use( http.get('/api/contacts', () => HttpResponse.json([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]), ), ) const wrapper = mountWithPlugins(ContactList) expect(wrapper.text()).toContain('Loading...') await flushPromises() expect(wrapper.text()).toContain('Alice') expect(wrapper.text()).toContain('Bob') }) ``` ### Error States Return a non-2xx response from MSW to exercise the error branch: ```ts it('handles errors', async () => { server.use( http.get('/api/contacts', () => HttpResponse.json({ message: 'Server Error' }, { status: 500 }), ), ) const wrapper = mountWithPlugins(ContactList) await flushPromises() expect(wrapper.text()).toContain('Request failed: 500') }) ``` ## Testing Mutations Example component: ```vue ``` Test it with a POST handler: ```ts import { http, HttpResponse } from 'msw' import { server } from './mocks/server' import CreateContact from './CreateContact.vue' it('creates a contact', async () => { server.use( http.post('/api/contacts', () => HttpResponse.json({ id: 3, name: 'Charlie' }), ), ) const wrapper = mountWithPlugins(CreateContact) await wrapper.find('input').setValue('Charlie') await wrapper.find('form').trigger('submit') await flushPromises() expect(wrapper.text()).toContain('Contact created!') }) ``` ## Tips * **`flushPromises()`**: Use `flushPromises()` from `@vue/test-utils` after actions that trigger async updates (initial query run, submit handlers, refetches). * **Inspecting cache state**: Use `useQueryCache()` inside tests to inspect or seed cache state. Since you're using a real `createPinia()`, the cache works exactly as it does in production. * **Simpler cases without MSW**: For narrow unit tests, pass a mocked query function directly: ```ts import { vi } from 'vitest' import { useQuery } from '@pinia/colada' const query = vi.fn().mockResolvedValue([{ id: 1, name: 'Alice' }]) const TestComponent = { setup() { const { data } = useQuery({ key: ['contacts'], query, }) return { data } }, template: `
    {{ data }}
    `, } const wrapper = mountWithPlugins(TestComponent) await flushPromises() expect(query).toHaveBeenCalledTimes(1) expect(wrapper.text()).toContain('Alice') ``` However, MSW is preferred since it tests the full request pipeline. --- --- url: /cookbook/tanstack-compat.md --- # TanStack Query Compatibility Plugin This plugin provides a compatibility layer for developers migrating from TanStack Query (Vue) to Pinia Colada, or for those who prefer the TanStack Query API style. It might not cover every single property or feature of TanStack Query, but it implements the most commonly used ones to make the transition smoother. If you find anything missing, please open a pull request to add it! ## Overview The plugin adds TanStack Query Vue v5 compatible properties to both `useQuery` and `useMutation` composables via TypeScript module augmentation. This means you get full type safety and IntelliSense support. ## Installation ```bash # or pnpm, or yarn, etc npm i @pinia/colada-plugin-tanstack-compat ``` ## Setup Register the plugin when installing Pinia Colada: ```ts import { createApp } from 'vue' import { createPinia } from 'pinia' import { PiniaColada } from '@pinia/colada' import { PiniaColadaTanStackCompat } from '@pinia/colada-plugin-tanstack-compat' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(PiniaColada, { plugins: [PiniaColadaTanStackCompat()], }) ``` ## Features ### useQuery Extensions The plugin adds 12 properties to every query: | Property | Type | Description | TanStack Equivalent | | --------------------- | ----------------------------------- | -------------------------------------------------- | --------------------- | | `isSuccess` | `ComputedRef` | `true` when status is `'success'` | `isSuccess` | | `isError` | `ComputedRef` | `true` when status is `'error'` | `isError` | | `isFetching` | `ComputedRef` | `true` when query is fetching (initial or refetch) | `isFetching` | | `isRefetching` | `ComputedRef` | `true` when refetching (not initial load) | `isRefetching` | | `isLoadingError` | `ComputedRef` | `true` when initial fetch failed (never had data) | `isLoadingError` | | `isRefetchError` | `ComputedRef` | `true` when refetch failed (had data before) | `isRefetchError` | | `isStale` | `ComputedRef` | `true` when data is considered stale | `isStale` | | `isFetched` | `ShallowRef` | `true` after first fetch completes | `isFetched` | | `isFetchedAfterMount` | `ShallowRef` | `true` after first fetch after component mount | `isFetchedAfterMount` | | `dataUpdatedAt` | `ShallowRef` | Timestamp (ms) of last successful data fetch | `dataUpdatedAt` | | `errorUpdatedAt` | `ShallowRef` | Timestamp (ms) of last error | `errorUpdatedAt` | | `fetchStatus` | `ComputedRef<'fetching' \| 'idle'>` | TanStack-style fetch status | `fetchStatus` | ### useMutation Extensions The plugin adds 7 properties to every mutation entry: | Property | Type | Description | TanStack Equivalent | | ---------------- | ---------------------- | ------------------------------------------ | ------------------- | | `isIdle` | `ComputedRef` | `true` when mutation has never been called | `isIdle` | | `isPending` | `ComputedRef` | `true` when mutation is in progress | `isPending` | | `isSuccess` | `ComputedRef` | `true` when status is `'success'` | `isSuccess` | | `isError` | `ComputedRef` | `true` when status is `'error'` | `isError` | | `submittedAt` | `ShallowRef` | Timestamp (ms) when mutation was submitted | `submittedAt` | | `dataUpdatedAt` | `ShallowRef` | Timestamp (ms) of last successful mutation | N/A | | `errorUpdatedAt` | `ShallowRef` | Timestamp (ms) of last error | N/A | ## Usage Examples ### Query Status Handling ```vue ``` ### Distinguishing Error Types ```vue ``` ### Tracking Fetch Activity ```vue ``` ### Mutation Status Tracking ```vue ``` ## Caveats and Limitations ### 1. Mutation Extensions Access **Issue**: Unlike `useQuery`, mutation extensions are NOT directly available on the `useMutation()` return value. **Reason**: Pinia Colada's `useMutation` doesn't spread `entry.ext` properties into its return object (unlike `useQuery` which does). **Workaround**: Access mutation extensions via the mutation cache: ```ts import { useMutation, useMutationCache } from '@pinia/colada' const { mutate } = useMutation({ key: ['myMutation'], mutation: async (data) => { /* ... */ }, }) const mutationCache = useMutationCache() // After calling mutate(), access extensions via cache const entry = mutationCache.getEntries({ key: ['myMutation'] })[0] const isPending = entry?.ext.isPending.value ``` ### 2. `fetchStatus: 'paused'` Not Supported **Issue**: TanStack Query's `fetchStatus` can be `'fetching' | 'paused' | 'idle'`. This plugin only supports `'fetching' | 'idle'`. **Reason**: Pinia Colada doesn't have a network mode concept that would pause queries when offline. **Workaround**: None currently. If you need offline support, consider handling it at the application level. ### 3. `isStale` Time-Based Updates **Issue**: The `isStale` computed doesn't automatically update when time passes. It only re-evaluates when query state changes. **Example**: ```ts // staleTime: 1000 (1 second) // After fetch: isStale = false // 2 seconds later (no state change): isStale still returns false until next state change // After refetch trigger: isStale correctly re-evaluates ``` **Reason**: Vue's reactivity system doesn't track time. The `isStale` getter depends on `Date.now()` which isn't reactive. **Workaround**: Trigger a state change to force re-evaluation, or use `entry.stale` directly when you need real-time staleness checking: ```ts const queryCache = useQueryCache() const entry = queryCache.get(['myQuery']) const isCurrentlyStale = entry?.stale // Getter evaluates on access ``` ### 4. Properties Not Included The following TanStack Query properties are NOT implemented: | Property | Reason | Alternative | | ------------------- | --------------------------------- | -------------------------------- | | `failureCount` | Requires retry plugin integration | Use `@pinia/colada-plugin-retry` | | `failureReason` | Requires retry plugin integration | Use `@pinia/colada-plugin-retry` | | `networkMode` | Not supported in Pinia Colada | N/A | | `select` | Different architecture | Use computed properties | | `structuralSharing` | Different architecture | N/A | | `isPaused` | No network mode | N/A | ## API Differences from TanStack Query ### Naming Differences | TanStack Query | Pinia Colada | Notes | | ------------------- | --------------------- | --------------------------------------- | | `queryKey` | `key` | Same purpose | | `queryFn` | `query` | Same purpose | | `isLoading` | `isPending` (queries) | Initial load state | | `status: 'loading'` | `status: 'pending'` | Initial state name | | `fetchStatus` | `asyncStatus` | Pinia Colada uses `'loading' \| 'idle'` | ### Conceptual Differences 1. **Status vs AsyncStatus**: Pinia Colada separates data status (`pending | success | error`) from async status (`loading | idle`), similar to TanStack Query v5's status/fetchStatus split. 2. **Mutation State**: TanStack mutations start as `'idle'`, while Pinia Colada mutations start as `'pending'`. The `isIdle` extension maps this correctly. 3. **Cache Keys**: Both use array-based keys, but Pinia Colada's key serialization may differ slightly. ## Migration Checklist When migrating from TanStack Query: * Install the plugin and register it * Replace `queryKey` with `key` * Replace `queryFn` with `query` * Replace `isLoading` checks with `isPending` for initial load * Update `status === 'loading'` to `status === 'pending'` * For mutations, access `isPending` via mutation cache if needed * Remove any `networkMode` options (not supported) * Replace `select` with Vue `computed` properties * If using retry features, install `@pinia/colada-plugin-retry` ## TypeScript Support The plugin provides full TypeScript support via module augmentation: ```ts // Types are automatically available after importing the plugin import { useQuery } from '@pinia/colada' const { isSuccess, dataUpdatedAt } = useQuery({ key: ['example'], query: async () => ({ id: 1 }), }) // isSuccess: ComputedRef // dataUpdatedAt: ShallowRef ``` ## Future Roadmap * Progressive feature disabling (opt-out of specific extensions) * Integration with retry plugin for `failureCount`/`failureReason` * Expose mutation extensions directly on `useMutation()` return (requires core change) --- --- url: /cookbook/migration-tvq.md --- # Migrating from `@tanstack/vue-query` to `@pinia/colada` This guide will help you migrate from `@tanstack/vue-query` to `@pinia/colada`. The two libraries have similar function names and API options, so it should be mostly a matter of updating the imports and adjusting the function names but there are still a couple of differences to be aware of. ::: info NOTE This guide is a work in progress and may not cover all the differences between the two libraries. Please, help us improve it by contributing if you find any missing information. ::: ## Different status values * `fetchStatus` is named `asyncStatus` * `asyncStatus` values are `idle` and `loading` instead of `idle` and `fetching` * Mutations also have an `asyncStatus` property and they match the query status values instead of having two different conventions ## Different defaults Most of the sensible defaults from `@tanstack/vue-query` are kept in `@pinia/colada`, but there are a few differences to be aware of: * Default `staleTime` is 5 seconds instead of 0 ## Different option names * `queryFn` is named `query` * `queryKey` is named `key` * `mutationFn` is named `mutation` ## API Differences | TanStack Vue Query Equivalent | Pinia Colada | Comment | | ----------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `refetch({ cancelRefetch: false })` | `refresh()` | See [Refetching Queries](../guide/queries.md#Refetching-Queries) | | `refetch({ throwOnError: true })` | `refetch(true)` | Same for `refresh()` | | `useQuery({ select })` | none | Use a `computed()` or write the logic within `query` instead. [See this discussion](https://github.com/posva/pinia-colada/discussions/113#discussioncomment-11311927) | | `useQuery({ refetchInterval })` | Auto Refetch plugin | Use the [`@pinia/colada-plugin-auto-refetch`](https://github.com/posva/pinia-colada/tree/main/plugins/auto-refetch) | | `useQuery({ retry })` | Retry plugin | Use the [`@pinia/colada-plugin-retry`](https://github.com/posva/pinia-colada/tree/main/plugins/retry) | | `useQuery().dataUpdatedAt` | Custom plugin or component code | [Custom plugin](../plugins/writing-plugins.md#Examples) | ### Using `ref` and `computed` in `queryKey` In Pinia Colada, `key` cannot **contain** a `computed` or `ref`, instead, it expects the key to be of type [`MaybeRefOrGetter`](https://vuejs.org/api/utility-types.html#maybereforgetter). In general, it simplifies keys handling and the function syntax is just so easy to use: ```ts useQuery({ queryKey: ['todos', { page: computed(() => route.query.id) }], // [!code --] key: () => ['todos', { page: route.query.page }], // [!code ++] // ... }) ``` ### Component-specific side effects in mutations TanStack's `mutate` function allows passing the promise resolution callbacks as mutation hooks. In Pinia Colada, to avoid *having multiple ways of doing the same thing*, use the `mutateAsync` method to handle those effects: ```ts mutate(todo, { // [!code --] onSuccess, // [!code --] onError, // [!code --] onSettled, // [!code --] }) // [!code --] mutateAsync(todo) .then((data) => { onSuccess(data) onSettled?.(data, null) }) .catch((err) => { onError(err) onSettled?.(undefined, err) }) ``` ## Differences in philosophy These differences are a bit more subtle and span across multiple layers of the library. ### Structural sharing TanStack implements a few rendering optimizations that are crucial in React but unnecessary in Vue. Pinia Colada does not implement these optimizations and instead relies on Vue's great reactivity system. The most notable difference is [Structural sharing](https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations#structural-sharing) which is explained in their React docs but barely mentioned in the Vue docs. In short, TanStack query partially updates parts of the object based on what is changed. This means that if your query returns the same data as before and you use a watcher on the data, it will not trigger the watcher. **This is not the case in Pinia Colada** as it uses [Shallow Refs](https://vuejs.org/api/reactivity-advanced.html#shallowref) to store data to get the best performance and simply replaces the value after each successful query. In Vue apps, this is rarely a problem, but if it is, can still avoid the watcher code by comparing the values: ```ts const { data } = useQuery({ key: ['todos'], query: fetchTodos, }) watch(data, (newData, oldData) => { if (!isSameData(newData, oldData)) { // do something with the new data } }) ``` ### Missing utilities Many utilities provided by TanStack are not included in Pinia Colada to let users get familiar with the [Query Cache](../advanced/query-cache.md) and implement their own utilities as needed. For example, there is no `useIsFetching` or `useIsMutating` composables, but you can easily implement them using the query cache (or mutation cache): ::: code-group ```ts [src/utils/pinia-colada.ts] twoslash import { useMutationCache, useQueryCache, type UseMutationEntryFilter, type UseQueryEntryFilter, } from '@pinia/colada' import { computed, type ComputedRef } from 'vue' /** * Returns a computed ref that indicates whether there are any ongoing queries * matching the provided filters. * @param filters - Optional filters to narrow down the queries to check. * @returns A computed ref that is true if there are ongoing queries, false otherwise. */ export function useIsLoading(filters?: UseQueryEntryFilter): ComputedRef { const queryCache = useQueryCache() return computed(() => queryCache.getEntries(filters).some((entry) => entry.asyncStatus.value === 'loading'), ) } /** * Returns a computed ref that indicates whether there are any ongoing mutations * matching the provided filters. * * @param filters - Optional filters to narrow down the mutations to check. * @returns A computed ref that is true if there are ongoing mutations, false otherwise. */ export function useIsMutating(filters?: UseMutationEntryFilter): ComputedRef { const mutationCache = useMutationCache() return computed(() => mutationCache.getEntries(filters).some((entry) => entry.asyncStatus.value === 'loading'), ) } ``` ::: Given the agnostic nature of TanStack Query, these utilities [are not straightforward to implement](https://github.com/TanStack/query/blob/main/packages/vue-query/src/useIsFetching.ts#L10-L38) but Pinia Colada's tight integration with Vue makes them *just workβ„’* For example, `useMutationState()` becomes `computed(() => mutationCache.getEntries(...).map(...))`. Differently from `useQueryState()`, `useMutationState()` always returns the state of multiple mutations so there are no helpers like `data`, `status`, etc If you are missing a helper utility, try to implement it using the query or mutation cache, in most cases, it's just calling `getEntries()` and then some `.map()`. These are not only easier to understand by humans without knowing the library but also easier to maintain and generate with AI tools. ### Reusable queries Pinia Colada sticks to Vue reactivity principles and encourages you to embrace them. Often, you will be able to pass a getter to options (often looks like `() => myOption.value`), this keeps reactivity working as expected. In TanStack Vue Query, you might use [`queryOptions`](https://tanstack.com/query/latest/docs/framework/vue/reference/queryOptions), in Pinia Colada, there is a similar feature but it's even more encouraged to use, it's [`defineQueryOptions`](../guide/queries.md#Organizing-Queries). There is also [`defineQuery`](../advanced/reusable-queries.md) which has no equivalent in TanStack Vue Query. In its simplest form, you can reuse queries like this: ```ts export const todosQuery = queryOptions({ // [!code --] // [!code --] queryKey: ['todos'], // [!code --] queryFn: getList, // [!code --] }) // [!code --] export const todosQuery = defineQueryOptions({ // [!code ++] // [!code ++] key: ['list'], // [!code ++] query: getList, // [!code ++] }) // [!code ++] ``` Check the [Organizing Queries](../guide/queries.md#Organizing-Queries) section for more information, you will seed that there is dynamic version of `defineQueryOptions` that allows passing reactive parameters and that using key factories is also encouraged. --- --- url: /cookbook/migrations.md --- # Migration Codemods Pinia Colada ships [ast-grep](https://ast-grep.github.io/) codemods to automate breaking-change migrations. Always **commit your changes before running a codemod** so you can review the diff. ## Running codemods With `pnpm dlx` (no global install): ```sh pnpm --package=@ast-grep/cli dlx ast-grep scan -r node_modules/@pinia/colada/codemods/rules/.yaml -i src ``` Or [install ast-grep globally](https://ast-grep.github.io/guide/quick-start.html#installation) and run: ```sh ast-grep scan -r node_modules/@pinia/colada/codemods/rules/.yaml -i src ``` Replace `src` with the directory that contains your source files and `` with the migration file name listed below. ## 0.13 β†’ 0.14 **File:** `migration-0-13-to-0-14.yaml` Global query options passed to the `PiniaColada` plugin have been moved under a `queryOptions` key. Calls without any options now require an empty object. Before: ```ts app.use(PiniaColada, { // [!code --] staleTime: 10_000, // [!code --] refetchOnWindowFocus: true, // [!code --] plugins: [/* ... */], // [!code --] }) // [!code --] app.use(PiniaColada) // [!code --] ``` After: ```ts app.use(PiniaColada, { // [!code ++] queryOptions: { // [!code ++] staleTime: 10_000, // [!code ++] refetchOnWindowFocus: true, // [!code ++] }, // [!code ++] plugins: [/* ... */], // [!code ++] }) // [!code ++] app.use(PiniaColada, {}) // [!code ++] ``` Run the codemod: ```sh ast-grep scan -r node_modules/@pinia/colada/codemods/rules/migration-0-13-to-0-14.yaml -i src ``` ## 0.21 β†’ 1.0 **File:** `migration-0-21-to-1-0.yaml` The two-parameter form of `useQuery` and `useQueryState` has been removed. Use a single function parameter instead. Before: ```ts useQuery(useContactList, () => ({ search: search.value })) // [!code --] useQueryState(useContactList, () => ({ search: search.value })) // [!code --] ``` After: ```ts useQuery(() => useContactList({ search: search.value })) // [!code ++] useQueryState(() => useContactList({ search: search.value }).key) // [!code ++] ``` Run the codemod: ```sh ast-grep scan -r node_modules/@pinia/colada/codemods/rules/migration-0-21-to-1-0.yaml -i src ``` --- --- url: /cookbook/migration-devtools-v2.md --- # Migrating Devtools from v1 to v2 In v2, you can open Pinia Colada directly in Vite DevTools or Nuxt DevTools. A standalone panel is also available. ![Pinia Colada v2 inspecting a query in Vite DevTools](/devtools-v2-vite.png) ## Replace the Vue components Remove the import and component from `App.vue` or your Nuxt root component: ::: code-group ```vue [Development] ``` ```vue [Production] ``` ::: Both components are removed in v2. Also remove any auto-import configuration for them. You can still use devtools in production: select the **Production** tab in the Vite configuration below. `PiniaColadaDevtools` is now a Vite plugin imported from `@pinia/colada-devtools/vite`. It is not a Vue component or a Vue plugin for `app.use()`. ## Vite Install the devtools and their peer dependencies. These plugins require Vite 7 or 8: ```sh pnpm add -D @pinia/colada-devtools @vitejs/devtools @vitejs/devtools-kit ``` Add the plugins to `vite.config.ts`. For Nuxt, use the [Nuxt setup](#Nuxt) below. Use Vite DevTools and its kit version 0.7.4 or later with DevFrame 0.10. ::: code-group ```ts{3-4,10-11} [Development] import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { DevTools } from '@vitejs/devtools' // [!code ++] import { PiniaColadaDevtools } from '@pinia/colada-devtools/vite' // [!code ++] export default defineConfig({ plugins: [ vue(), // Add Vite DevTools before the Pinia Colada panel DevTools(), // [!code ++] PiniaColadaDevtools(), // [!code ++] ], }) ``` ```ts{12,17} [Production] import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { DevTools } from '@vitejs/devtools' import { PiniaColadaDevtools } from '@pinia/colada-devtools/vite' export default defineConfig({ plugins: [ vue(), DevTools({ build: { // Include the devtools assets in the app build withApp: true, // [!code ++] }, }), PiniaColadaDevtools({ // Enable the Pinia Colada panel in production production: true, // [!code ++] }), ], }) ``` ::: Register `PiniaColadaDevtools()` after `DevTools()`. Open Vite DevTools and select Pinia Colada. Your saved v1 button position, open state, and panel height are not kept. Devtools are enabled only during development by default. The production configuration adds the devtools assets to the build and enables the Pinia Colada panel. For production, also load the devtools script in the built app: ```html [index.html] ``` Replace the leading `/` with your Vite `base` if it differs from the default. Deploy the full build output, including the devtools assets. The [playground configuration](https://github.com/posva/pinia-colada/blob/main/playground/vite.config.ts) shows how to add this script only during the build. ## Nuxt Update `@pinia/colada-nuxt` to the latest version. Use the Vite builder and Nuxt DevTools 4 or later, then install the devtools: ```sh pnpm add -D @pinia/colada-devtools ``` Enable devtools in `nuxt.config.ts`: ```ts{5} [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@pinia/nuxt', '@pinia/colada-nuxt'], devtools: { // Show the Pinia Colada panel in Nuxt DevTools enabled: true, // [!code ++] }, }) ``` The module registers the panel automatically during development. Open Nuxt DevTools and select Pinia Colada. ![The Pinia Colada entry in the Nuxt DevTools sidebar](/devtools-v2-nuxt.png) ## Vue DevTools Support for Pinia Colada v2 in Vue DevTools is still in progress. For now, use Vite DevTools or the standalone plugin below. ## Standalone To keep a separate Pinia Colada devtools panel, install the devtools and the standalone plugin's peer dependencies: ```sh pnpm add -D @pinia/colada-devtools @devframes/hub @devframes/hub-ui @devframes/vite ``` Add the plugin to `vite.config.ts`: ```ts{3,9} [vite.config.ts] import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { PiniaColadaDevtoolsStandalone } from '@pinia/colada-devtools/standalone' // [!code ++] export default defineConfig({ plugins: [ vue(), // Add the standalone devtools interface PiniaColadaDevtoolsStandalone(), // [!code ++] ], }) ``` The standalone plugin only runs during development. For production, use the [Vite configuration](#Vite) above. MCP support is included through `@devframes/agentic`. The standalone host enables MCP; a custom hub controls MCP through its own `mcp` option. ## Projects without Vite The included integrations require Vite 7 or 8. If you use another build tool, keep v1 until a compatible integration is available, or build a custom Devframe host with the exports described below. ## Update custom integrations and imports The `/panel` and `/shared` package exports are removed. If you use them in a custom integration, update these imports: | v1 API | v2 replacement | | --- | --- | | `PiniaColadaDevtools` from `@pinia/colada-devtools` | `PiniaColadaDevtools()` from `@pinia/colada-devtools/vite` | | `PiniaColadaProdDevtools` from `@pinia/colada-devtools` | [Vite production configuration](#Vite) | | `DevtoolsPanel` from `@pinia/colada-devtools/panel` | A host mounts the panel from `piniaColadaDevframe` | | Helpers and types from `@pinia/colada-devtools/shared` | No public replacement; remove these imports | The integrations now use [Devframe](https://devfra.me/). For a custom host, import `piniaColadaDevframe` and `piniaColadaDevframeDock` from `@pinia/colada-devtools`. Register the devframe and its dock client script with your host. The client script is available at `@pinia/colada-devtools/client-script` and connects the inspected app to the panel. The panel now runs in a separate page. Remove the `pinia-colada-devtools-panel` custom element registration and any code that uses the old panel or message channel. Use the [Devframe host APIs](https://devfra.me/guide/hub) for custom integrations. The included Vite and standalone plugins handle this setup for you. --- --- url: /guide/cancelling-queries.md --- # Canceling Queries * The `signal` properties * Use only to discard results: * New queries of the same key * Cancelled with cancelQueries * Differences between invalidating and canceling queries --- --- url: /plugins/official.md --- # Official plugins * [Query Hooks (built-in)](./query-hooks.md) * [Auto refetch](./auto-refetch.md) * [Retry](./retry.md) * [Delay](./delay.md) * [Cache persister](./cache-persister.md) --- --- url: /errors/pinia_colada_r0010.md --- # PINIA\_COLADA\_R0010: removing an active query entry * Level: warning (dev only) ## What happened `queryCache.remove(entry)` was called while a component or effect scope still uses the entry. Removing an active entry is unsupported. Its consumers can keep references to an entry that is no longer in the cache, which can cause unexpected behavior. ## How to fix it Wait until all consumers stop using the entry before removing it. If you need to refresh an active query, invalidate it instead: ```ts await queryCache.invalidateQueries({ key: entry.key, exact: true }) ``` ## Common causes * Clearing the cache while a component that uses the query is still mounted * Removing an entry from a query cache plugin while it still has consumers --- --- url: /guide/placeholder-data.md --- # Placeholder Data perfect for pagination and searches --- --- url: /cookbook/query-hooks.md --- # Query Hooks You might notice that `useQuery` does not support `onSuccess`, `onError`, or `onSettled` callbacks. This is intentional. Local callbacks introduce side effects directly into the data layer and are triggered for each component using the query. This can lead to duplicated behavior and makes it harder to keep side effects separate from data logic so **they simply do not exist** in query options. ## Local The simplest way is by using `watch` in the component: ```ts const { error } = useQuery({ key: ['todos'], query: () => fetchTodos(), }) watch(error, (e) => { if (e) { toast.error(e.message ?? 'Something went wrong') } }) ``` While this approach works, it can lead to duplicated code across components. ## Recommended For global side effects, you can use the [query hooks plugin](/plugins/official/query-hooks.md) to centralize side-effect handling (toasts, logging, analytics…) with the `meta` field to keep `useQuery` declarative: ```ts useQuery({ key: ['todos'], query: () => fetchTodos(), meta: { errorMessage: 'Failed to load todos', }, }) ``` --- --- url: /guide/setup.md --- # Setup --- --- url: /guide/typescript.md --- # TypeScript * Mostly inferred * Because of the amount of type params in generics, when providing, one you have to provide all of them or they become the default value (e.g TContext becomes an empty object)