React Query的数据转换

写程序,永远都要面对数据结构和算法。使用React Query去获取数据时,很多时候我们也需要进行数据转化,这篇博文就是介绍在使用React Query时如何进行数据转换。

原文地址: React Query Data Transformations

欢迎来到“关于react-query我不得不说的事情”的第2部分。随着我越来越多地参与到这个库及其社区当中时,我观察到大家很多常问的共性问题。 最初,我想将它们全部写在一篇大文章中,但后来决定将它们分解为更易于管理的多个文章。 第一个是关于一个非常常见和重要的任务:数据转换。

数据转换

让我们直面这个问题-我们大多数人都没有使用GraphQL。如果你这样做了,那么你会很高兴,因为你可以轻松地以你想要的格式获得你的数据。

但是,如果你正在使用REST,则会受到后端返回内容的限制。 那么在使用react-query时,如何以及在哪里最好地转换数据? 在软件开发中唯一值得一试的答案也适用于此:

按需进行
— 每一个开发者都会这样说

以下是3+1种方法,可以根据各自的优缺点来转换数据:

0.在后端进行

如果我们能这样做的话,这是我所最喜欢的方法。如果后端以我们想要的结构返回数据,我们就不需要做任何事情。虽然这在许多情况下听起来就不切实际,例如在使用公共REST API时,也很有可能是在企业应用程序中。如果我们控制后端这完全可以为我们的返回你确切想要的数据,这完全是按照我们期望的方式交付数据。

前端无需任何工作
不是什么时候都可以这样做

1.在queryFn中进行转换

queryFn是我们传递给useQuery的函数。它期望我们返回一个Promise,结果数据会在查询缓存中保存。 但这并不意味着我们必须使用后端提供的数据结构去保存我们所需要的数据。 我们也可以在执行此操作之前对其进行转换:

1
2
3
4
5
6
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)

在前端,我们可以使用这些数据“就像它来自后端一样”。在我们的代码中,我们实际上不会使用非大写的待办事项名称。当然我们也将无法访问原始结构。如果我们查看react-query-devtools,我们将看到转换后的结构。如果我们去查看网络跟踪,我们将看到原始结构。 这可能会令人困惑,所以请记住这一点。

此外,react-query在这里不会为我们做任何优化。每次执行数据请求时,我们的转换都会运行。如果运行代价非常大的转换,请考虑其他选择之一。 一些公司还会使用一个共享api层来抽象数据获取,因此我们可能无法访问该层来进行转换。

从协同开发的角度来说,“非常接近后端”
转换后的数据结构在会存到缓存中,因此我们将无法访问原始的数据结构
每次获取数据时都会执行
如果我们有无法自由修改的共享 api层,这完全就不可行了

2.在渲染函数中

第1部分所述,如果我们创建自定义钩子,我们就可以轻松地在那里进行转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()),
  }
}

就目前而言,这不仅会在每次fetch函数运行时运行,实际上只会在每次渲染时运行(即使是那些不涉及数据提取的渲染)。这可能根本不是问题,但如果是,我们可以使用useMemo进行优化。谨慎的地添加依赖项,让useMome有尽可能少的依赖项。除非确实发生了变化(在这种情况下我们想要重新进行转换),否则queryInfo中的数据引用将是不变的,但queryInfo本身是会发生变化的。 如果我们添加queryInfo作为我们依赖项,转换将在每次渲染时再次运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export const useTodosQuery = () => {

  const queryInfo = useQuery(['todos'], fetchTodos)
  return {
    ...queryInfo,
    // ❌ 不要这样做
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo]
    ),

    // ✅ 正确的使用方式 
    data: React.useMemo(
      () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),
      [queryInfo.data]
    ),
  }
}

特别是如果我们的自定义钩子中有额外的逻辑与数据转换相结合,这是一个不错的选择。 请注意,数据可能是未定义的,因此在使用它时使用可选链(Typescript的特性之一,在使用Javascript时,强烈反对使用三目预算代替,强烈建议使用Ramda的组合操作来代替)。

可以使用useMemo来优化
无法在 devtools 中检查确切的结构
语法有些复杂
数据可能是未定义的

3.使用select选项

v3引入了内置选择器,也可用于转换数据:

1
2
3
4
5
export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    select: (data) => data.map((todo) => todo.name.toUpperCase()),
  })

选择器只会在数据存在时被调用,所以我们不必关心这里会有undefined。像上面这样的选择器也将在每次渲染时运行,因为函数标识发生了变化(它是一个内联函数)。如果我们的数据转换代价非常大,我们可以使用useCallback或通过将其提取为外部固定函数引用来保存这个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const transformTodoNames = (data: Todos) =>
  data.map((todo) => todo.name.toUpperCase())


export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    // ✅ 使用固定的函数引用
    select: transformTodoNames,
  })


export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {

    // ✅ 使用useCallback进行缓存
    select: React.useCallback(
      (data: Todos) => data.map((todo) => todo.name.toUpperCase()),
      []
    ),
  })

此外,select选项还可用于仅订阅部分数据。 这就是使这种方法真正独一无二的原因。 考虑以下示例:

1
2
3
4
5
6
7
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))

在这里,我们通过将自定义过滤函数传递给我们的useTodosQuery,创建了一个类似useSelector的API。 自定义钩子仍然像以前一样工作,因为如果不传递过滤函数,select将是未定义的,它将返回所有的数据。

但是如果我们传递一个过滤函数,我们现在只订阅了过滤函数的结果。 这非常强大,因为这意味着即使我们更新了一个todo的名称,我们通过useTodosCount只订阅计数的组件是不会重新渲染。因为计数没有改变,所以react-query可以选择不通知这个观察者更新🥳(请注意,这里有点简化,技术上不完全正确 — 我将在第3部分中更详细地讨论渲染优化)。

可以获得库的最优化
可以定于局部数据
数据结构可能会因为观察者不同而完全不同
共享的结构会执行两次(我还将在第3部分中更详细地讨论这一点)

译注

前端,除了需要完成一些UI上美观,动画外,更多的时候都是在处理数据和操作流程(业务流程)。任何时候,程序都是算法加数据。所以能更好的处理数据,就能更好的做好业务,有效的利用好数据结构,能大大减少一些非必要的时间消耗,从而提升整个前端的流畅度。