queryKey Factory 模式
用工厂函数集中管理 TanStack Query 的 queryKey,避免硬编码字符串,保证缓存键的一致性和可维护性。
#type / pattern
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: 缓存失效模式
- 实践: 从 useState 迁移
queryKey Factory 模式
核心问题
当项目中有几十个 useQuery 调用时,queryKey 散落在各个组件中:
// 组件 A
useQuery({ queryKey: ['users'], ... });
useQuery({ queryKey: ['user', id], ... });
// 组件 B
queryClient.invalidateQueries({ queryKey: ['users'] });
// 组件 C
queryClient.setQueryData(['user', id], newData);
问题:
- 拼写错误不会报错(
['user', id]vs['users', id]) - 改 key 结构时要全局搜索替换
- 无法一眼看出哪些 key 属于同一个领域
Factory 模式
基本结构
// query-keys.ts
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: Record<string, unknown>) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
使用方式
// 获取列表
useQuery({ queryKey: userKeys.list({ page: 1, role: 'admin' }), queryFn: ... });
// 获取详情
useQuery({ queryKey: userKeys.detail(userId), queryFn: ... });
// 失效所有用户相关缓存
queryClient.invalidateQueries({ queryKey: userKeys.all });
// 只失效列表
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
// 更新单个详情缓存
queryClient.setQueryData(userKeys.detail(userId), newData);
层级结构
userKeys.all → ['users']
userKeys.lists() → ['users', 'list']
userKeys.list({ page: 1 }) → ['users', 'list', { page: 1 }]
userKeys.details() → ['users', 'detail']
userKeys.detail('123') → ['users', 'detail', '123']
利用 TanStack Query 的前缀匹配机制:
invalidateQueries({ queryKey: userKeys.all })会使所有以['users']开头的缓存失效invalidateQueries({ queryKey: userKeys.details() })只会使['users', 'detail', ...]失效
空值 Key 模式
当 id 可能为 null 时(如 routeConversationId),需要处理空值情况:
// ❌ 非空断言只是 TypeScript 语法,不改变运行时
useQuery({
queryKey: ['conversation', conversationId!], // conversationId 可能是 null
queryFn: () => fetchConversation(conversationId!),
enabled: !!conversationId,
});
// 当 conversationId 为 null 时,queryKey 是 ['conversation', null]
// 虽然 enabled: false 阻止了请求,但 key 不够干净
// ✅ 用空值 key 明确语义
export const consultationKeys = {
all: ['consultation'] as const,
conversations: () => [...consultationKeys.all, 'conversations'] as const,
conversation: (id: string) => [...consultationKeys.all, 'conversation', id] as const,
conversationEmpty: () => [...consultationKeys.all, 'conversation', 'empty'] as const,
session: (id: string) => [...consultationKeys.all, 'session', id] as const,
sessionEmpty: () => [...consultationKeys.all, 'session', 'empty'] as const,
};
// 使用时
useQuery({
queryKey: conversationId
? consultationKeys.conversation(conversationId)
: consultationKeys.conversationEmpty(),
queryFn: () => fetchConversation(conversationId!),
enabled: !!conversationId,
});
这样 TypeScript 类型和运行时行为都一致:有 id 时用真实 key,没 id 时用 empty 占位 key。
多领域 Factory
// 各领域独立的 keys 文件
export const consultationKeys = {
all: ['consultation'] as const,
conversations: () => [...consultationKeys.all, 'conversations'] as const,
conversation: (id: string) => [...consultationKeys.all, 'conversation', id] as const,
session: (id: string) => [...consultationKeys.all, 'session', id] as const,
};
export const trainingKeys = {
all: ['training'] as const,
plans: () => [...trainingKeys.all, 'plans'] as const,
plan: (id: string) => [...trainingKeys.all, 'plan', id] as const,
sessions: (planId: string) => [...trainingKeys.all, 'plan', planId, 'sessions'] as const,
};
export const assessmentKeys = {
all: ['assessment'] as const,
reports: () => [...assessmentKeys.all, 'reports'] as const,
report: (id: string) => [...assessmentKeys.all, 'report', id] as const,
};
常见错误
层级不一致导致误删
// ❌ conversations() 的 key 长度也是 2,可能被误匹配
const consultationKeys = {
all: ['consultation'] as const,
conversations: () => [...consultationKeys.all, 'conversations'] as const, // ['consultation', 'conversations']
session: (id: string) => [...consultationKeys.all, 'session', id] as const, // ['consultation', 'session', id]
};
// 用 predicate 精确删除时可能误删
queryClient.removeQueries({
queryKey: consultationKeys.all,
predicate: (q) => q.queryKey.length > 1, // conversations 也被删了
});
// ✅ 精确指定
queryClient.removeQueries({ queryKey: [...consultationKeys.all, 'conversation'] });
queryClient.removeQueries({ queryKey: [...consultationKeys.all, 'session'] });
忘记 as const
// ❌ 类型推导丢失
const userKeys = {
all: ['users'], // 类型是 string[],不是 readonly ['users']
};
// ✅ 保持字面量类型
const userKeys = {
all: ['users'] as const,
};
参数顺序不一致
// ❌ 不同地方参数顺序不同
useQuery({ queryKey: ['users', page, role], ... });
queryClient.invalidateQueries({ queryKey: ['users', role, page] }); // 不会命中!
// ✅ 用 factory 保证一致
useQuery({ queryKey: userKeys.list({ page, role }), ... });
queryClient.invalidateQueries({ queryKey: userKeys.all });
速查:何时用哪种 key
| 场景 | key 结构 | 说明 |
|---|---|---|
| 无参数列表 | ['users', 'list'] | 最简单 |
| 带过滤的列表 | ['users', 'list', { filters }] | 过滤参数作为对象 |
| 单个详情 | ['users', 'detail', id] | id 作为最后一位 |
| 关联子资源 | ['users', id, 'posts'] | 嵌套结构 |
| 全局失效 | userKeys.all | 前缀匹配,一次失效所有 |