Skip to content

Quick Start

Pinia Colada is the perfect companion to Pinia to handle async state management in your Vue applications. It wil remove the need to write boilerplate code for data fetching and transparently bring caching, deduplication, invalidation, 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 project.

Installation

Install Pinia Colada alongside Pinia 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
import { 
createApp
} from 'vue'
import {
createPinia
} from 'pinia'
import {
PiniaColada
} from '@pinia/colada'
const
app
=
createApp
({})
app
.
use
(
createPinia
())
app
.
use
(
PiniaColada
)
app
.
mount
('#app')

Usage

Let's see a quick example and cover the basics of queries, mutations and query invalidation.

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 param-less query function that returns the data:

vue
<script setup lang="ts">
import { 
useQuery
} from '@pinia/colada'
import {
getAllProducts
} from '@/api/products'
import
ProductItem
from '@/components/ProductItem.vue'
const {
state
:
productList
,
asyncStatus
,
} =
useQuery
({
key
: ['products-list'],
query
:
getAllProducts
,
}) </script>
  • The key is a serializable array that uniquely identifies the query. The array allows to establish a hierarchy of keys that can be invalidated at once.
  • The query function takes no arguments because that allows Pinia Colada to automatically run it 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 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:

      statusdataerror
      'pending'undefinednull
      'success'definednull
      'error'undefined or defineddefined
  • asyncStatus: the async status of the query. It's either 'idle' or 'loading' if the query is currently being fetched.

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:

vue
<script setup lang="ts">
import { 
useQuery
} from '@pinia/colada'
import {
getAllProducts
} from '@/api/products'
import
ProductItem
from '@/components/ProductItem.vue'
const { // when using multiple queries in the same component, // it is convenient to rename `data`
state
:
productList
,
asyncStatus
,
} =
useQuery
({
key
: ['products-list'],
query
:
getAllProducts
,
}) </script> <template> <
main
>
<
LoadingIndicator
v-if="
asyncStatus
=== 'loading'" />
<
div
v-if="
productList
.
error
">
<
ErrorMessage
:
error
="
productList
.
error
" />
</
div
>
<
div
v-else-if="
productList
.
data
">
<
div
v-for="
product
in
productList
.
data
"
:
key
="
product
.
id
">
<!-- we could add a prefetch here : @mouseover="prefetch(product.id)" --> <
ProductItem
:
product
="
product
" />
</
div
>
</
div
>
</
main
>
</template>
vue
<script setup lang="ts">
import { 
useQuery
} from '@pinia/colada'
import {
useRoute
} from 'vue-router'
import {
getProductById
} from '@/api/products'
import
ProductItemDetail
from '@/components/ProductItemDetail.vue'
// in this example we use the url params in the query to fetch // a specific product const
route
=
useRoute
()
const {
state
:
product
,
asyncStatus
,
} =
useQuery
({
// `key` can be made dynamic by providing a function or a reactive property
key
: () => ['products',
route
.
params
.
id
as string],
query
: () =>
getProductById
(
route
.
params
.
id
as string),
}) </script> <template> <
main
>
<
LoadingIndicator
v-if="
asyncStatus
=== 'loading'" />
<
div
v-if="
product
.
error
">
<
ErrorMessage
:
error
="
product
.
error
" />
</
div
>
<
div
v-else
class
="flex flex-wrap">
<
ProductItemDetail
:
product
="
product
.
data
" />
</
div
>
</
main
>
</template>

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 only require a mutation function, the key is optional:

vue
<script setup lang="ts">
import { 
useMutation
} from '@pinia/colada'
import { type Contact,
patchContact
} from '@/api/contacts'
import
ContactDetail
from '@/components/ContactDetail.vue'
const {
mutate
,
state
,
asyncStatus
,
} =
useMutation
({
mutation
: (
contact
: Contact) =>
patchContact
(
contact
),
}) </script>

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 😉.

As you can see, 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
<script setup lang="ts">
import { 
useMutation
,
useQueryCache
} from '@pinia/colada'
import { type Contact,
patchContact
} from '@/api/contacts'
import
ContactDetail
from '@/components/ContactDetail.vue'
const
props
=
defineProps
<{
contact
: Contact }>()
// we use the query cache to invalidate queries const
queryCache
=
useQueryCache
()
const {
mutate
:
updateContact
,
state
,
asyncStatus
,
} =
useMutation
({
mutation
: (
contact
: Contact) =>
patchContact
(
contact
),
onSettled
(
updatedContact
,
error
,
contact
) {
// invalidate the contact list and detail queries // they will be refetched automatically if they are being used
queryCache
.
invalidateQueries
({
key
: ['contacts-list'] })
queryCache
.
invalidateQueries
({
key
: ['contacts',
contact
.
id
] })
}, }) </script> <template> <
main
>
<
LoadingIndicator
v-if="
asyncStatus
=== 'loading'" />
<
div
v-if="
state
.
error
">
<
ErrorMessage
:
error
="
state
.
error
" />
</
div
>
<
div
v-else>
<
ContactDetail
:
contact
="
props
.
contact
"
:
is-updating
="
asyncStatus
=== 'loading'"
@
update
="
updateContact
"
/> </
div
>
</
main
>
</template>

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 ✨.

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.

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:

Released under the MIT License.