select 与数据转换

TanStack Query 的 select 选项允许在 cache 层面做数据转换,避免不必要的 re-render,是性能优化的重要手段。

#type / howto #status / evergreen #tech / dev / frontend #resource / react

[!info] related notes

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[](原始类型)
创建于 2026/7/3 更新于 2026/7/15