pinia-colada

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pinia 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

status
vs
asyncStatus

  • status
    :
    'pending' | 'success' | 'error'
    — data state
  • asyncStatus
    :
    'loading' | 'idle'
    — network activity
Pattern: Skeleton when
status === 'pending'
. Subtle spinner when
asyncStatus === 'loading' && status === 'success'
.
  • status
    :
    'pending' | 'success' | 'error'
    — 数据状态
  • asyncStatus
    :
    'loading' | 'idle'
    — 网络活动状态
模式建议:
status === 'pending'
时显示骨架屏;当
asyncStatus === 'loading' && status === 'success'
时显示细微的加载动画。

refresh()
vs
refetch()

refresh()
vs
refetch()

  • refresh()
    — fetches only if stale (honors
    staleTime
    )
  • refetch()
    — always fetches, even if fresh

  • 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
    as const
    for type inference
  • Reactive keys:
    key: () => productKeys.detail(id.value)
  • Never read reactive values in
    query()
    without including them in the key

键是依赖项,而非标签。需要包含
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()
— 组件绑定型

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()
— 可复用配置项(推荐使用)

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()
— 跨组件共享状态

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
invalidateQueries
keeps mutation "loading" until refetch completes.

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() })
  },
}))
提示: 等待
invalidateQueries
完成可让变更操作保持“加载中”状态,直到重新获取数据完成。

Invalidation Strategy

缓存失效策略

ActionInvalidate
Create
root()
/
list()
queries
Update
detail(id)
+ affected lists
DeleteLists, optionally remove detail
Avoid over-invalidating. Prefer hierarchical invalidation with minimal scope.

操作需失效的查询
新增
root()
/
list()
查询
更新
detail(id)
+ 受影响的列表查询
删除列表查询,可选择性移除详情查询
避免过度失效。 优先使用最小范围的层级化失效策略。

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

通过UI实现(更简单)——加载时从
mutation.variables
渲染数据

Via 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"
  • useQueryCache()
    outside setup
    — requires Vue injection context
  • Stable keys for dynamic queries — always use reactive key functions

  • 在Pinia Store中使用查询——Store很少会被销毁,导致查询变成“永久存在”
  • 在setup外部使用
    useQueryCache()
    ——该方法需要Vue的注入上下文
  • 为动态查询使用固定键——应始终使用响应式键函数

Troubleshooting

问题排查

ProblemSolution
Query doesn't refetch when X changesInclude X in the key, use
key: () => [...]
Invalidation doesn't workCheck key hierarchy, use exact matching or predicate
Old data while loadingExpected behavior —
status: success
+
asyncStatus: loading
means background refresh
Optimistic rollback bugsCancel queries before cache write, use race-safe rollback

问题解决方案
当X变化时查询未重新获取数据将X包含在键中,使用
key: () => [...]
缓存失效不生效检查键的层级结构,使用精确匹配或断言函数
加载时显示旧数据这是预期行为——
status: success
+
asyncStatus: loading
表示后台正在刷新数据
乐观更新回滚出现bug在写入缓存前取消查询,使用竞态安全的回滚机制

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
    status
    vs
    asyncStatus
    correctly
  • Call out pitfalls
Claude 应始终遵循以下要求:
  • 从键的设计开始
  • 解释选择某一模式的原因
  • 优先使用最小范围的缓存失效
  • 选择正确的基础方法(
    useQuery
    defineQueryOptions
    useMutation
    等)
  • 正确处理
    status
    asyncStatus
  • 指出潜在的陷阱