select 与数据转换
TanStack Query 的 select 选项允许在 cache 层面做数据转换,避免不必要的 re-render,是性能优化的重要手段。
#type / howto
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: 派生状态模式
- 关联: placeholderData 与 keepPreviousData
select 与数据转换
核心问题
API 返回的数据结构不一定适合组件直接使用:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers, // 返回 { users: User[], total: number, page: number }
});
// 组件只需要活跃用户的名字列表
const activeNames = data?.users
.filter(u => u.isActive)
.map(u => u.name) ?? [];
问题:每次 data 引用变化(即使内容没变),activeNames 都会重新计算,导致下游组件 re-render。
select 的作用
select 在 cache 层面做转换,只有转换结果变化时才触发 re-render:
const { data: activeNames } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.users
.filter(u => u.isActive)
.map(u => u.name),
});
// data 现在直接是 string[],不是原始的 API 响应
// 原始数据仍在缓存中,只是组件拿到的是转换后的版本
工作原理
queryFn 返回原始数据 → 写入 cache
↓
select 转换
↓
组件拿到转换后的数据
原始数据变化 → select 重新执行
├─ 转换结果引用相等 → 不 re-render ✅
└─ 转换结果引用不同 → re-render
关键:select 只影响组件拿到的数据,不影响 cache 里存的数据。
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers, // cache 里存的是 User[]
select: (data) => data[0], // 组件拿到的是 User
});
// data 的类型是 User(select 后的类型)
// 但 queryClient.setQueryData(['users'], ...) 接受 User[](原始类型)
// queryClient.getQueryData(['users']) 也返回 User[](原始类型)
使用场景
过滤与映射
const { data: activeUsers } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.filter(u => u.isActive),
});
提取嵌套字段
const { data: messages } = useQuery({
queryKey: ['conversation', id],
queryFn: () => fetchConversation(id),
select: (data) => data.conversation.messages,
});
计算派生值
const { data: stats } = useQuery({
queryKey: ['orders'],
queryFn: fetchOrders,
select: (orders) => ({
total: orders.length,
revenue: orders.reduce((sum, o) => sum + o.amount, 0),
average: orders.reduce((sum, o) => sum + o.amount, 0) / orders.length,
}),
});
格式化数据
const { data: chartData } = useQuery({
queryKey: ['metrics', range],
queryFn: () => fetchMetrics(range),
select: (data) => data.map(d => ({
x: new Date(d.timestamp).toLocaleDateString(),
y: d.value,
label: `${d.value}%`,
})),
});
与 useMemo 的对比
不用 select
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
// ❌ 每次组件 re-render 都重新计算
const activeNames = data?.filter(u => u.isActive).map(u => u.name) ?? [];
// ✅ 用 useMemo 优化
const activeNames = useMemo(
() => data?.filter(u => u.isActive).map(u => u.name) ?? [],
[data]
);
用 select
// ✅ 更简洁,且自动做引用相等检查
const { data: activeNames } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.filter(u => u.isActive).map(u => u.name),
});
| 方式 | 优点 | 缺点 |
|---|---|---|
select | 自动引用相等检查,写法简洁 | 只能用于 useQuery 的数据 |
useMemo | 通用,可用于任何计算 | 需要手动维护依赖数组 |
| 直接计算 | 最简单 | 每次 re-render 都计算 |
select 与 structuralSharing
TanStack Query 默认使用结构共享(structural sharing):新旧数据如果结构相同,保持旧引用。
原始数据: { users: [A, B, C] }
新数据: { users: [A, B, C] } // 内容相同
→ 保持旧引用,下游组件不 re-render
select 在此基础上再加一层:
原始数据变化 → 结构共享检查
├─ 结构相同 → 不触发 select
└─ 结构不同 → 执行 select
├─ 结果引用相同 → 不 re-render
└─ 结果引用不同 → re-render
常见错误
select 返回新对象导致过度 re-render
// ❌ 每次都创建新对象,引用总是不同
const { data } = useQuery({
queryKey: ['user', id],
queryFn: fetchUser,
select: (data) => ({ name: data.name }), // 每次都是新对象
});
// ✅ 返回原始引用或稳定引用
const { data } = useQuery({
queryKey: ['user', id],
queryFn: fetchUser,
select: (data) => data.name, // 原始值,引用稳定
});
select 中做副作用
// ❌ select 中发请求或修改外部状态
select: (data) => {
analytics.track('data_transformed'); // 副作用!
return data.map(transform);
}
// ✅ select 只做纯计算
select: (data) => data.map(transform),
select 类型不匹配
// ❌ 返回类型和 queryKey 不匹配,缓存行为异常
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers, // 返回 User[]
select: (data) => data[0], // 返回 User
});
// data 的类型是 User,但缓存里存的是 User[]
// setQueryData 时要注意
// ✅ 类型自动推导
// data: User | undefined(select 后的类型)
// queryClient.setQueryData(['users'], ...) 接受 User[](原始类型)