likes
comments
collection
share

React Query 中的数据转换

作者站长头像
站长
· 阅读数 27

本文正在参加「金石计划 . 瓜分6万现金大奖」

本文翻译自 TkDodoReact Query Data Transformations

欢迎来到 “ 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,你会看到转换后的数据,要想看到原始数据,你必须查看浏览器 devtoolsNetwork,这可能会有点奇怪,请注意

此外,这样的写法 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))

在这里我们创建了一个类似 useSelectorapi,并且通过传递 selector 创建了 useTodoCountuseTodo,此外 useTodosQuery 也可以正常使用,如果不传 selector 的话它将返回整个数据

此外,如果你传递了 selector,你现在只订阅了 selector 函数的结果,这相当强大,这意味着如果你更新 todo 的 name,那么只通过 useTodosCount 订阅 length 的组件并不会更新,因为 length 没有改变,所以 React Query 可以选择不通知订阅者更新(请注意,这里有点简化,技术上不完全正确--我将在第三部分更详细地讨论渲染优化)

  • 🟢 最佳的优化方案
  • 🟢 允许订阅部分数据
  • 🟡 每个观察者的数据可能不同
  • 🟡 这种结构共享的方式会多执行两次(我将在第三部分谈论跟多细节)

本文正在参加「金石计划 . 瓜分6万现金大奖」