Server state has different characteristics than local state. This episode covers Pinia Colada for fetching, caching, and deduplication, then integrating stores with Vue Router for reading route params and guards, and using external composables alongside Pinia.

Since episode 7, our async actions have stored fetch results in state. That pattern works, but server state — data that comes from APIs — has special needs: caching, deduplication, retry, and invalidation. Handling all of that manually is a path to bugs.
Episode 14 covers the official solution in the Pinia ecosystem for this problem: Pinia Colada for async server state, integrating stores with Vue Router, and how to combine external composables with stores.
Pinia Colada is an official library in the Pinia ecosystem for server state. It handles fetching, caching, and deduplication with clear keys:
import { defineQuery } from '@pinia/colada'
export const useUserQuery = defineQuery({
key: ['user', 'profile'],
query: () => fetch('/api/profile').then((res) => res.json()),
})defineQuery({ key, query }) defines a query that's cached based on key. In a component, the result can be read through the data and status states and the refetch method:
<script setup lang="ts">
const { data, status, refetch } = useUserQuery()
</script>
<template>
<div v-if="status === 'loading'">Loading...</div>
<div v-else>{{ data?.name }}</div>
</template>Compared to manual actions, the advantage is this: two components using the same query share one cache and won't do a double fetch — that's deduplication, which is hard to reproduce manually.
A simple guideline used by production teams:
useUserQuery() // server state: cache and dedup
useCartStore() // client state: user interactionMixing both is common: a store action calls refetch from a query to sync after a mutation, or a query reads store state as a parameter.
Stores often need route information. Inside setup, use useRoute; outside setup, import the router instance:
import router from '@/router'
export const useDetailStore = defineStore('detail', {
actions: {
async loadCurrent() {
const id = router.currentRoute.value.params.id
this.current = await fetch(`/api/item/${id}`).then((r) => r.json())
},
},
})router.currentRoute.value.params.id reads the route parameter directly from the router instance. A more idiomatic alternative: read id via useRoute() in the component, then pass it as an action argument.
Stores and composables aren't rivals — the two can complement each other:
import { useLocalStorage } from '@vueuse/core'
export const usePrefsStore = defineStore('prefs', () => {
const fontSize = useLocalStorage('font-size', 16)
function setSize(size: number) {
fontSize.value = size
}
return { fontSize, setSize }
})useLocalStorage('font-size', 16) inside a Setup store combines a VueUse composable with global state. This pattern can only be done with a Setup store, as discussed in episode 4.
Info
An important decision: don't store all server state into Pinia. Use Colada or Vue Query for that layer, and Pinia for client state — this split keeps the architecture healthy at scale.
Episode 14 extends Pinia into modern architecture. You now understand Pinia Colada for async server state with caching and dedup, how to combine stores with Vue Router, and how to integrate external composables inside a Setup store.
Key takeaways:
useRoute() or the router instance.In the next episode, episode 15, we'll discuss security and best practices — avoiding the persistence of tokens and secrets, choosing secure storage, sanitizing API data before it enters a store, and best practices for state classification, single responsibility, and role-based access control.