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 |
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
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 β
pnpm add @pinia/colada
pnpm dlx nuxi module add @pinia/colada-nuxtnpm install @pinia/colada
npx nuxi module add @pinia/colada-nuxtyarn add @pinia/colada
yarn dlx nuxi module add @pinia/colada-nuxtOr manually by installing it and adding it to your nuxt.config.ts:
pnpm add @pinia/colada @pinia/colada-nuxtnpm install @pinia/colada @pinia/colada-nuxtyarn add @pinia/colada @pinia/colada-nuxt// 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:
npx nuxi module add piniaConfiguration β
You can configure the Pinia Colada plugin by creating a colada.options.ts file at the root of your project.
// colada.options.ts
import type { PiniaColadaOptions } from '@pinia/colada'
export default {
// Options here
} satisfies PiniaColadaOptionsThese options will get passed to the PiniaColada Vue plugin. This allows you to add options like 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:
<script lang="ts" setup>
// Nuxt: await required for SSR
const { data } = await useFetch('/api/products')
</script>With Pinia Colada, useQuery uses onServerPrefetch internally, so queries automatically run and await on the server without explicit await:
<script lang="ts" setup>
// Pinia Colada: no await needed, SSR works automatically
const { data } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
})
</script>How it works β
- On the server,
useQueryautomatically registers viaonServerPrefetch - The query runs and awaits during server-side rendering
- Data is serialized to the payload and hydrated on the client
- No extra code neededβit just works
When you still need await β
Using await with useQuery is still useful when you want to block client side navigation until data loads:
<script lang="ts" setup>
const { data, refresh } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
})
// Block navigation until products load
await refresh()
</script>Without await, the page renders immediately (showing loading states), and data populates when ready.
Another alternative is to use Data Loaders, which connect the data fetching lifecycle to Vue Router (and therefore Nuxt) navigation system.
Migration Guide β
useFetch β useQuery β
<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/products')
const { data, isPending, error, refresh } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
})
</script>Key differences:
pendingβisPending- Add a unique
keyfor caching - Wrap the fetch in
queryoption - Remove
await:- SSR works automatically
- Add
await refresh()if you want to block navigation until data loads
useAsyncData β useQuery β
<script lang="ts" setup>
const { data } = await useAsyncData(
'products',
() => $fetch('/api/products'),
{
getCachedData(key, nuxtApp) {
return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
},
},
)
// Pinia Colada: caching is automatic
const { data } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
staleTime: 1000 * 60, // optional: data fresh for 1 minute
})
</script>Adding mutations β
Check out the full Mutations Guide for more details.
Shared data across components β
Before (Nuxt): Pass data down via props from page components
<!-- pages/products.vue -->
<script lang="ts" setup>
const { data: products } = await useFetch('/api/products')
</script>
<template>
<!-- Must pass products to every child that needs it -->
<ProductList :products="products" />
<ProductSummary :products="products" />
</template>After (Pinia Colada): Any component can access the same cached data
<!-- components/ProductList.vue -->
<script lang="ts" setup>
// Same key = same cached data, no props needed
const { data: products } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
})
</script><!-- components/ProductSummary.vue -->
<script lang="ts" setup>
import { useQuery } from '@pinia/colada'
// Shares cache with ProductList, no duplicate requests
const { data: products } = useQuery({
key: ['products'],
query: () => $fetch('/api/products'),
})
</script>Check the Query Organization Guide 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 to serialize them:
// 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),
)
})