本文正在参加「金石计划 . 瓜分6万现金大奖」
欢迎来到 “ React Query
不得不说的事”第二部分,随着我越来越多的参与 React Query
及其社区的工作,我观察到一些人们经常询问的模式,起初,我想把它们写进一个大的文章里,但是后来我决定把它们拆分成开来,这样更容易管理,第一篇的主题非常常见且重要:数据转换
数据转换
让我们面对现实,我们大多数人都没有使用 GraphQL
,如果你使用了,那么恭喜,你可以拿到你想要的数据格式
如果你使用 REST API
,你受限于服务端返回的数据,那么在 React Query
中如何做数据转换呢,以下有 3+1 种方法,以及它们各自的优缺点
0. 在服务端
这是我最喜欢的方法,如果服务端完全按照我们想要的数据结构返回,那么我们就不需要做什么了,虽然这在很多场景下听起来不切实际,比如使用公共的 REST API
,但是在企业的应用程序中还是相当可能实现的,如果你可以影响服务端给你返回你期望的数据
- 🟢 前端没有工作
- 🔴 并不是百分之百可行
1. 在 queryFn 中
queryFn
是你传递给 useQuery
的函数,它期望你返回一个 Promise
,并且最终的数据会存放在缓存中,但是这并不意味这你必须返回服务端的数据,你可以在传递给 useQuery
之前进行转换
const fetchTodos = async (): Promise<Todos> => {
const response = await axios.get('todos')
const data: Todos = response.data
return data.map((todo) => todo.name.toUpperCase())
}
export const useTodosQuery = () => useQuery(['todos'], fetchTodos)
在前端,你可以把它们看做是从服务端来的数据一样,你的代码中不会出现使用非大写的 todo.name
,你也无法访问原始的数据,如果你查看react-query-devtools
,你会看到转换后的数据,要想看到原始数据,你必须查看浏览器 devtools
的 Network
,这可能会有点奇怪,请注意
此外,这样的写法 React Query
不能为你提供任何优化,每次执行 fetch 的时候你的数据转换都会执行,如果它很昂贵,请考虑其它选择,有些公司可能有共享的 api 层用来做数据的抽象,所以你你可能没有办法访问这一层
- 🟢 非常”靠近“服务端,
- 🟡 转化后的数据存在缓存内,所以你无法访问原始数据
- 🔴 每次 fetch 时都会执行
- 🔴 如果你有一个不可修改的共享 api 层,这种方式不可用
2. 在渲染函数中
就像第一篇中建议的,如果你创建了一个自定义的钩子,你可以很容易的在那里做数据转换
const fetchTodos = async (): Promise<Todos> => {
const response = await axios.get('todos')
return response.data
}
export const useTodosQuery = () => {
const queryInfo = useQuery(['todos'], fetchTodos)
return {
...queryInfo,
data: queryInfo.data?.map((todo) => todo.name.toUpperCase()),
}
}
这将会在你的组件每次渲染时执行(即使是那些不涉及数据获取的渲染),这可能根本不是一个问题,但是如果你的转换很昂贵的话,你可以用 useMemo
来优化,此外要注意尽可能缩窄你定义的依赖关系,queryInfo.data
是稳定的,但是 queryInfo
不是,它会在每次渲染时都创建一个新的
export const useTodosQuery = () => {
const queryInfo = useQuery(['todos'], fetchTodos)
return {
...queryInfo,
// 🚨 don't do this - the useMemo does nothing at all here!
data: React.useMemo(
() => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
[queryInfo]
),
// ✅ correctly memoizes by queryInfo.data
data: React.useMemo(
() => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
[queryInfo.data]
),
}
}
特别是如果你的自定义钩子中有一些逻辑需要去数据转换相结合,这种方法是一个不错的选择,此外请注意,data
可能是 undefined
,在使用时需要使用可选链
- 🟢 通过
useMemo
进行优化 - 🟡 无法在 devtools 中看到准确的数据
- 🔴 更复杂一点的语法
- 🔴
data
可能未 undefined
3. 使用 select 选项
v3 引入了新的 select 选项,可用来做数据转换
export const useTodosQuery = () =>
useQuery(['todos'], fetchTodos, {
select: (data) => data.map((todo) => todo.name.toUpperCase()),
})
select
只有在 data
存在的时候才会被调用,所以你不用担心它是 undefined
,上面的代码中 select
函数会在每次渲染的是调用,因为它是一个内联函数,每次渲染都会被创建,所以我们可以使用 useCallback
或者将它提取出来来保证它的稳定
const transformTodoNames = (data: Todos) =>
data.map((todo) => todo.name.toUpperCase())
export const useTodosQuery = () =>
useQuery(['todos'], fetchTodos, {
// ✅ uses a stable function reference
select: transformTodoNames,
})
export const useTodosQuery = () =>
useQuery(['todos'], fetchTodos, {
// ✅ memoizes with useCallback
select: React.useCallback(
(data: Todos) => data.map((todo) => todo.name.toUpperCase()),
[]
),
})
此外,使用 select
选项还可以做到只订阅部分数据,这也是这种方法的独到之处,思考如下代码
export const useTodosQuery = (select) =>
useQuery(['todos'], fetchTodos, { select })
export const useTodosCount = () => useTodosQuery((data) => data.length)
export const useTodo = (id) =>
useTodosQuery((data) => data.find((todo) => todo.id === id))
在这里我们创建了一个类似 useSelector 的 api
,并且通过传递 selector
创建了 useTodoCount
和 useTodo
,此外 useTodosQuery
也可以正常使用,如果不传 selector
的话它将返回整个数据
此外,如果你传递了 selector
,你现在只订阅了 selector
函数的结果,这相当强大,这意味着如果你更新 todo 的 name,那么只通过 useTodosCount
订阅 length
的组件并不会更新,因为 length
没有改变,所以 React Query
可以选择不通知订阅者更新(请注意,这里有点简化,技术上不完全正确--我将在第三部分更详细地讨论渲染优化)
- 🟢 最佳的优化方案
- 🟢 允许订阅部分数据
- 🟡 每个观察者的数据可能不同
- 🟡 这种结构共享的方式会多执行两次(我将在第三部分谈论跟多细节)
本文正在参加「金石计划 . 瓜分6万现金大奖」