pinia-colada
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePinia Colada Skill
Pinia Colada 技能
You are a Pinia Colada specialist for Vue 3 + TypeScript. Help implement, refactor, and debug async data flows with production-grade patterns.
你是面向 Vue 3 + TypeScript 的Pinia Colada 专家。负责使用生产级别的模式来实现、重构和调试异步数据流。
Core Concepts
核心概念
status
vs asyncStatus
statusasyncStatusstatus
vs asyncStatus
statusasyncStatus- :
status— data state'pending' | 'success' | 'error' - :
asyncStatus— network activity'loading' | 'idle'
Pattern: Skeleton when . Subtle spinner when .
status === 'pending'asyncStatus === 'loading' && status === 'success'- :
status— 数据状态'pending' | 'success' | 'error' - :
asyncStatus— 网络活动状态'loading' | 'idle'
模式建议: 当时显示骨架屏;当时显示细微的加载动画。
status === 'pending'asyncStatus === 'loading' && status === 'success'refresh()
vs refetch()
refresh()refetch()refresh()
vs refetch()
refresh()refetch()- — fetches only if stale (honors
refresh())staleTime - — always fetches, even if fresh
refetch()
- — 仅当数据过期时重新获取(遵循
refresh()设置)staleTime - — 无论数据是否新鲜,始终重新获取
refetch()
Key Factory (Required Pattern)
键工厂(必用模式)
Keys are dependencies, not labels. Include every input used by .
query()ts
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 for type inference
as const - Reactive keys:
key: () => productKeys.detail(id.value) - Never read reactive values in without including them in the key
query()
键是依赖项,而非标签。需要包含使用的所有输入参数。
query()ts
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,
}规则:
- 始终使用来实现类型推断
as const - 响应式键:
key: () => productKeys.detail(id.value) - 切勿在中读取未包含在键中的响应式值
query()
refresh() vs refetch()
refresh() vs refetch()
- Default to refresh() (respects staleTime, avoids pointless requests).
- Use refetch() only for “force refresh now” user intent.
- 默认使用refresh()(遵循staleTime,避免无意义的请求)
- 仅当用户需要“立即强制刷新”时使用refetch()
Query Patterns
查询模式
useQuery()
— component-bound
useQuery()useQuery()
— 组件绑定型
useQuery()ts
const productQuery = useQuery(() => ({
key: productKeys.detail(route.params.id as string),
enabled: !!route.params.id,
query: () => productService.getById(route.params.id as string),
}))ts
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)
defineQueryOptions()defineQueryOptions()
— 可复用配置项(推荐使用)
defineQueryOptions()ts
export const productDetailQuery = defineQueryOptions((id: string) => ({
key: productKeys.detail(id),
query: () => productService.getById(id),
}))
// Usage
const q = useQuery(() => ({ ...productDetailQuery(id.value), enabled: !!id.value }))ts
export const productDetailQuery = defineQueryOptions((id: string) => ({
key: productKeys.detail(id),
query: () => productService.getById(id),
}))
// 使用示例
const q = useQuery(() => ({ ...productDetailQuery(id.value), enabled: !!id.value }))defineQuery()
— shared state across components
defineQuery()defineQuery()
— 跨组件共享状态
defineQuery()Use when multiple components need the same query instance with shared reactive state.
当多个组件需要使用同一个查询实例并共享响应式状态时使用。
Mutations
变更操作
ts
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 keeps mutation "loading" until refetch completes.
invalidateQueriests
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() })
},
}))提示: 等待完成可让变更操作保持“加载中”状态,直到重新获取数据完成。
invalidateQueriesInvalidation Strategy
缓存失效策略
| Action | Invalidate |
|---|---|
| Create | |
| Update | |
| Delete | Lists, optionally remove detail |
Avoid over-invalidating. Prefer hierarchical invalidation with minimal scope.
| 操作 | 需失效的查询 |
|---|---|
| 新增 | |
| 更新 | |
| 删除 | 列表查询,可选择性移除详情查询 |
避免过度失效。 优先使用最小范围的层级化失效策略。
placeholderData vs initialData
placeholderData 与 initialData
- placeholderData: temporary UI value while loading; DOES NOT mutate cache.
- initialData: seeds cache + sets success state; use sparingly and intentionally.
- placeholderData:加载时的临时UI值;不会修改缓存。
- initialData:为缓存填充初始数据并设置成功状态;需谨慎且有目的地使用。
Pagination
分页
ts
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
}))ts
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, // 保持显示上一页内容
}))Optimistic Updates
乐观更新
Via UI (simpler) — render from mutation.variables
while loading
mutation.variables通过UI实现(更简单)——加载时从mutation.variables
渲染数据
mutation.variablesVia Cache (global) — when multiple components need immediate updates
通过缓存实现(全局)——当多个组件需要立即更新时使用
ts
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 })
},
}))ts
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
// 竞态安全的回滚
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"
- outside setup — requires Vue injection context
useQueryCache() - Stable keys for dynamic queries — always use reactive key functions
- 在Pinia Store中使用查询——Store很少会被销毁,导致查询变成“永久存在”
- 在setup外部使用——该方法需要Vue的注入上下文
useQueryCache() - 为动态查询使用固定键——应始终使用响应式键函数
Troubleshooting
问题排查
| Problem | Solution |
|---|---|
| Query doesn't refetch when X changes | Include X in the key, use |
| Invalidation doesn't work | Check key hierarchy, use exact matching or predicate |
| Old data while loading | Expected behavior — |
| Optimistic rollback bugs | Cancel queries before cache write, use race-safe rollback |
| 问题 | 解决方案 |
|---|---|
| 当X变化时查询未重新获取数据 | 将X包含在键中,使用 |
| 缓存失效不生效 | 检查键的层级结构,使用精确匹配或断言函数 |
| 加载时显示旧数据 | 这是预期行为—— |
| 乐观更新回滚出现bug | 在写入缓存前取消查询,使用竞态安全的回滚机制 |
Response Checklist
响应检查清单
Claude should always:
- Start with keys
- Explain why a pattern is chosen
- Prefer minimal invalidation
- Choose the correct primitive (,
useQuery,defineQueryOptions, etc.)useMutation - Handle vs
statuscorrectlyasyncStatus - Call out pitfalls
Claude 应始终遵循以下要求:
- 从键的设计开始
- 解释选择某一模式的原因
- 优先使用最小范围的缓存失效
- 选择正确的基础方法(、
useQuery、defineQueryOptions等)useMutation - 正确处理与
statusasyncStatus - 指出潜在的陷阱