欢送来到“对于 react-query 我不得不说的一些事件”的第二章节。随着我越来越深刻这个库以及他的社区,我发现一些人们常常会问到的问题。最开始,我打算在一篇超长的文章外面把这些都讲清楚,最终我还是决定将他们拆分成一些有意义的主题。明天第一个主题是一个很广泛然而很重要的事件:数据转换。
数据转换
咱们不得不面对这个问题 - 大部分的人并没有应用 GraphQL。如果你应用了,那么祝贺你,因为你能够申请到你冀望的数据格式。
如果你在应用 REST 格调的 API,你就必须受限于后端返回的数据格式。所以在应用 react-query 的时候咱们应该在什么中央通过什么形式来进行数据转换呢?答案只有一个:看状况。
上面列举出四种进行数据转换的形式,以及他们的优缺点:
0. 在后端
这是我最喜爱的形式,如果你有决定权的话。如果后端返回的数据结构是你所冀望的话,那么你就什么都不必做了。然而在很多场景这并不太事实,比方一些公共的 REST API,特地是在企业级利用中。如果你能够让后端针对每一个具体的场景都有一个对应的接口,那么能够返回你冀望的数据结构。
- 长处:
前端什么都不必做 - 毛病:
并不是所有状况下都能做到
1. 在查问函数中
查问函数是你传给 useQuery
的函数。他会返回一个 Promise,最终返回的数据会被存在缓存中。然而这并不意味着你只能依照后端给你的数据结构来返回数据。你能够在返回之前进行数据转换:
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 名字了。同时你也拿不到数据的原始构造了。如果你查看 react-query-devtools,你会看到转换之后的构造。如果你查看网络申请,你能够看到原始的数据结构。这个可能会有点让人感到困惑,所以不要忘了你在代码外面解决了数据结构。
同时,在这里 react-query 并不会做什么优化。也就是说每一次 fetch 被执行的时候,你的转换逻辑都会被执行。如果转换逻辑很简单,须要考虑一下其余转换形式。一些公司在前端会有一个公共的 API 层来形象数据获取,所以你可能没方法在这个形象层外面做你的数据转换。
- 长处:
和 API 调用绑定在一起,对下层无感知 - 毛病:
在每次数据申请的时候都会运行
如果你有一个你无奈批改的公共的 API 层,这个形式不太可行 - 其余:
存储在缓存中的是转换之后的数据结构,所以你没方法拿到原始的数据结构
2. 在 render 函数中
正如第一章节中介绍的,你能够自定义一个 hook,那么你能够很不便的在这个 hook 里做数据转换:
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()),
}
}
正如代码逻辑所示,数据转换不会在每次数据查问的时候运行,然而会在每次 render 的时候运行(即便这次 render 并没有触发数据申请)。这看起来这不是什么大问题,如果你在意的话,你能够通过 useMemo
来进行优化,同时尽可能只定义真正须要的依赖列表。queryInfo 中的 data
是援用稳固的除非数据真的产生了变动,然而 queryInfo
就不是了。如果你把 queryInfo
作为你的依赖,那么转换逻辑就会在每次 render 的时候运行:
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]
),
}
}
特地是当你在自定义 hook 中有一些额定的逻辑来帮助进行数据转换的时候,这是一个很好的抉择。须要留神的是 data 有可能是 undefined,所以请应用可选链式拜访来获取 data 中的数据。
- 长处:
能够通过 useMemo 进行优化 - 毛病
写法有一些艰涩
data 可能会是 undefined - 其余
确切的数据结构无奈在 devtool 中展现
3. 应用 select 配置
v3 引入了内置的 selector,能够用它来进行数据转换:
export const useTodosQuery = () =>
useQuery(['todos'], fetchTodos, {select: (data) => data.map((todo) => todo.name.toUpperCase()),
})
selector 只会在 data 存在的时候被调用,所以你不必放心 undefiend 的问题。像下面的 selector 会在每次 render 的时候被执行,因为函数表达式变动了(因为这是一个内联函数)。如果转换逻辑比较复杂,你能够应用 useCallback 来进行 memoize,或者把他形象到一个稳固的函数援用中:
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 配置也能够被用来订阅 data 中的局部数据。这使得这一数据转换实现形式变得特地。看看上面这个例子:
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 到 useTodosQuery 中。这个自定义 hook 依然能够像之前一样工作,如果你没有传 select,会返回整个数据。
然而如果你传了 selector,你就只会订阅 selector 返回的局部数据。这是很有用的,因为这意味着如果咱们更新了一个 todo 的名字,只通过 useTodosCount 订阅了 count 的组件并不会从新渲染。count 没有发生变化,所以 react-query 能够抉择不告诉这部分数据的订阅者(留神这里说得很容易,然而具体实现不齐全跟这个形容一样,我会在第三局部渲染优化中聊一聊这部分内容)
- 长处:
最佳优化
反对局部订阅 - 其余:
每个订阅者的数据可能都不一样
原文地址:https://tkdodo.eu/blog/react-…