Pinia Colada - Smart Data Fetching for Vue
Status: Production Ready ✅ | Last Updated: 2025-11-28 Latest Version: @pinia/colada@0.17.9 | Dependencies: Vue 3.5.17+, Pinia 2.2.6+ or 3.0+
Quick Start (5 Minutes)
1. Install Dependencies
For Vue Projects:
bun add @pinia/colada pinia # preferred
# or: bun add @pinia/colada pinia
For Nuxt Projects:
bun add @pinia/nuxt @pinia/colada-nuxt # install both Pinia and Pinia Colada modules
# or: bun add @pinia/nuxt @pinia/colada-nuxt
Why this matters:
- Pinia Colada requires Pinia 2.2.6+ or 3.0+ as peer dependency
- Nuxt module handles SSR serialization automatically
- Vue 3.5.17+ required for optimal reactivity
2. Set Up Pinia Colada Plugin
For Vue Projects:
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(PiniaColada, {
// Optional: Configure defaults
query: {
staleTime: 5000, // 5 seconds
gcTime: 5 * 60 * 1000, // 5 minutes (garbage collection)
refetchOnMount: true,
refetchOnWindowFocus: false,
},
})
app.mount('#app')
For Nuxt Projects:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@pinia/nuxt', // Must be before @pinia/colada-nuxt
'@pinia/colada-nuxt',
],
// Optional: Configure Pinia Colada
piniaColada: {
query: {
staleTime: 5000,
gcTime: 5 * 60 * 1000,
},
},
})
CRITICAL:
- For Nuxt:
@pinia/nuxtmust be listed before@pinia/colada-nuxt - Plugin must be registered after Pinia instance
- Configuration is optional - sensible defaults provided
3. Create First Query
<script setup lang="ts">
import { useQuery } from '@pinia/colada'
interface Todo {
id: number
title: string
completed: boolean
}
async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error('Failed to fetch todos')
}
return response.json()
}
const {
data, // Ref<Todo[] | undefined>
isPending, // Ref<boolean> - initial loading
isLoading, // Ref<boolean> - any loading (including refetch)
error, // Ref<Error | null>
refresh, // () => Promise<void> - manual refetch
} = useQuery({
key: ['todos'],
query: fetchTodos,
})
</script>
<template>
<div>
<div v-if="isPending">Loading todos...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else-if="data">
<li v-for="todo in data" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</div>
</template>
CRITICAL:
- Query
keymust be an array (or getter returning array) for consistent caching - Query
queryis the async function that fetches data - Throw errors in query function for proper error handling
isPendingistrueonly on initial load,isLoadingincludes refetches
4. Create First Mutation
<script setup lang="ts">
import { useMutation, useQueryCache } from '@pinia/colada'
interface NewTodo {
title: string
}
async function createTodo(newTodo: NewTodo) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!response.ok) throw new Error('Failed to create todo')
return response.json()
}
const queryCache = useQueryCache()
const {
mutate, // (variables: NewTodo) => Promise<void>
mutateAsync, // (variables: NewTodo) => Promise<Result>
isPending, // Ref<boolean>
error, // Ref<Error | null>
data, // Ref<Result | undefined>
} = useMutation({
mutation: createTodo,
// Invalidate todos query after mutation succeeds
async onSettled({ id }) {
await queryCache.invalidateQueries({ key: ['todos'] })
},
})
function handleAddTodo(title: string) {
mutate({ title })
}
</script>
<template>
<form @submit.prevent="handleAddTodo(newTitle)">
<input v-model="newTitle" required />
<button type="submit" :disabled="isPending">
{{ isPending ? 'Adding...' : 'Add Todo' }}
</button>
<div v-if="error">Error: {{ error.message }}</div>
</form>
</template>
Why this works:
onSettledruns after success or error, perfect for invalidationinvalidateQueriesmarks matching queries as stale and refetches active onesmutateis fire-and-forget,mutateAsyncreturns Promise for await- Mutations don't cache by default (correct behavior for writes)
Critical Rules
Always Do
✅ Include all variables used in query function in the key ✅ Throw errors in query/mutation functions for proper error handling ✅ Use useQueryCache() for invalidation in mutations ✅ Use isPending for initial load, isLoading for any loading state ✅ Await invalidateQueries() in onSettled when you need data fresh before continuing ✅ Use placeholderData for paginated queries to avoid flashing ✅ Snapshot cache with getQueryData before optimistic updates ✅ Return context from onMutate for rollback in onError ✅ Configure staleTime and gcTime at plugin level for app-wide defaults ✅ Use reusable composables for queries instead of inline useQuery
Never Do
❌ Never use plain strings as keys - always use arrays ❌ Never return undefined from query function - throw errors instead ❌ Never mutate data.value directly - it's readonly ❌ Never forget to invalidate related queries after mutations ❌ Never use onSuccess in queries (not available, use watch instead) ❌ Never forget to await mutateAsync() - it returns a Promise ❌ Never skip cancelQueries before optimistic updates (causes race conditions) ❌ Never use getQueryData without checking for undefined ❌ Never invalidate queries in onMutate (do it in onSettled) ❌ Never hardcode URLs - use environment variables for API base URLs
Top 5 Errors Prevention
This skill prevents 12 documented errors. Here are the top 5:
Error #1: Query Not Refetching After Mutation
Error: Data doesn't update in UI after successful mutation Prevention: Always use invalidateQueries in onSettled:
useMutation({
mutation: createTodo,
async onSettled() {
await queryCache.invalidateQueries({ key: ['todos'] })
},
})
See: references/error-catalog.md #1
Error #2: Race Condition with Optimistic Updates
Error: Optimistic update gets overwritten by in-flight request Prevention: Always call cancelQueries in onMutate:
onMutate(id) {
cache.cancelQueries({ key: ['todos'] })
// Then do optimistic update
}
See: references/error-catalog.md #2
Error #3: SSR Hydration Mismatch
Error: Hydration completed but contains mismatches Prevention: Set refetchOnMount: false for SSR queries
useQuery({
key: ['todos'],
query: fetchTodos,
refetchOnMount: false, // Prevents SSR hydration mismatch
})
See: references/error-catalog.md #3
Error #4: Query Key Not Reactive
Error: Query doesn't refetch when variable changes Prevention: Use function for reactive keys:
// ❌ Wrong - static key
key: ['todos', id.value]
// ✅ Correct - reactive key
key: () => ['todos', id.value]
See: references/error-catalog.md #4
Error #5: Nuxt Module Order Wrong
Error: PiniaColada plugin not found or SSR errors Prevention: Always put @pinia/nuxt first:
export default defineNuxtConfig({
modules: [
'@pinia/nuxt', // MUST be first
'@pinia/colada-nuxt', // Then Colada
],
})
See: references/error-catalog.md #10
For complete error catalog (all 12 errors): See references/error-catalog.md
Using Bundled Resources
References (references/)
Detailed guides loaded when needed:
- **`referenc