pinia-colada
SKILL.md
Pinia Colada Skill
You are a Pinia Colada specialist for Vue 3 + TypeScript. Help implement, refactor, and debug async data flows with production-grade patterns.
Core Concepts
status vs asyncStatus
status:'pending' | 'success' | 'error'— data stateasyncStatus:'loading' | 'idle'— network activity
Pattern: Skeleton when status === 'pending'. Subtle spinner when asyncStatus === 'loading' && status === 'success'.
refresh() vs refetch()
refresh()— fetches only if stale (honorsstaleTime)refetch()— always fetches, even if fresh
Key Factory (Required Pattern)
Keys are dependencies, not labels. Include every input used by query().
export const productKeys = {
root: () => ['products'] as const,
list: (params: { q?: string; page?: number }) => ['products', 'list', params] as const,
detail: (id: string) => ['products', 'detail', id] as const,
}
Rules:
- Always use
as constfor type inference - Reactive keys:
key: () => productKeys.detail(id.value) - Never read reactive values in
query()without including them in the key
refresh() vs refetch()
- Default to refresh() (respects staleTime, avoids pointless requests).
- Use refetch() only for “force refresh now” user intent.
Query Patterns
useQuery() — component-bound
const productQuery = useQuery(() => ({
key: productKeys.detail(route.params.id as string),
enabled: !!route.params.id,
query: () => productService.getById(route.params.id as string),
}))
defineQueryOptions() — reusable options (preferred)
export const productDetailQuery = defineQueryOptions((id: string) => ({
key: productKeys.detail(id),
query: () => productService.getById(id),
}))
// Usage
const q = useQuery(() => ({ ...productDetailQuery(id.value), enabled: !!id.value }))
defineQuery() — shared state across components
Use when multiple components need the same query instance with shared reactive state.
Mutations
const queryCache = useQueryCache()
const updateProduct = useMutation(() => ({
mutation: (vars: { id: string; name: string }) => productService.update(vars),
onSuccess: async (data, vars) => {
await queryCache.invalidateQueries({ key: productKeys.detail(vars.id) })
await queryCache.invalidateQueries({ key: productKeys.root() })
},
}))
Tip: Awaiting invalidateQueries keeps mutation "loading" until refetch completes.
Invalidation Strategy
| Action | Invalidate |
|---|---|
| Create | root() / list() queries |
| Update | detail(id) + affected lists |
| Delete | Lists, optionally remove detail |
Avoid over-invalidating. Prefer hierarchical invalidation with minimal scope.
placeholderData vs initialData
- placeholderData: temporary UI value while loading; DOES NOT mutate cache.
- initialData: seeds cache + sets success state; use sparingly and intentionally.
Pagination
const page = ref(1)
const listQuery = useQuery(() => ({
key: productKeys.list({ q: search.value, page: page.value }),
query: () => productService.list({ q: search.value, page: page.value }),
placeholderData: (prev) => prev, // Keep previous page visible
}))
Optimistic Updates
Via UI (simpler) — render from mutation.variables while loading
Via Cache (global) — when multiple components need immediate updates
const mutate = useMutation(() => ({
onMutate: async (vars) => {
const key = productKeys.detail(vars.id)
await queryCache.cancelQueries({ key })
const oldValue = queryCache.getQueryData(key)
const newValue = oldValue ? { ...oldValue, ...vars } : oldValue
queryCache.setQueryData(key, newValue)
return { key, oldValue, newValue }
},
mutation: (vars) => productService.update(vars),
onError: (err, vars, ctx) => {
if (!ctx) return
// Race-safe rollback
if (queryCache.getQueryData(ctx.key) === ctx.newValue) {
queryCache.setQueryData(ctx.key, ctx.oldValue)
}
},
onSettled: async (data, err, vars, ctx) => {
if (ctx) await queryCache.invalidateQueries({ key: ctx.key })
},
}))
Anti-Patterns
- Queries in Pinia stores — stores rarely destroy, queries become "immortal"
useQueryCache()outside setup — requires Vue injection context- Stable keys for dynamic queries — always use reactive key functions
Troubleshooting
| Problem | Solution |
|---|---|
| Query doesn't refetch when X changes | Include X in the key, use key: () => [...] |
| Invalidation doesn't work | Check key hierarchy, use exact matching or predicate |
| Old data while loading | Expected behavior — status: success + asyncStatus: loading means background refresh |
| Optimistic rollback bugs | Cancel queries before cache write, use race-safe rollback |
Response Checklist
Claude should always:
- Start with keys
- Explain why a pattern is chosen
- Prefer minimal invalidation
- Choose the correct primitive (
useQuery,defineQueryOptions,useMutation, etc.) - Handle
statusvsasyncStatuscorrectly - Call out pitfalls
Weekly Installs
7
Repository
pilag6/skills-c…ude-codeFirst Seen
Feb 10, 2026
Security Audits
Installed on
opencode7
gemini-cli5
github-copilot5
codex5
kimi-cli5
amp5