发布于 2026-01-06 1 阅读
0

React 查询数据转换 数据转换

React 查询数据转换

数据转换

欢迎来到“关于 react-query 的一些想法”系列的第二部分。随着我对这个库及其社区的了解越来越深入,我注意到大家经常会问到一些问题。起初,我打算把它们全部写成一篇文章,但后来决定分成几个部分来讨论。第一个部分是关于一个非常常见且重要的任务:数据转换。

数据转换

说实话,我们大多数人都没有使用 GraphQL。如果你在使用,那你真是太幸运了,因为你可以随心所欲地以你想要的格式请求数据。

但如果你使用 REST,就会受到后端返回结果的限制。那么,在使用 react-query 时,如何以及在哪里才能最好地转换数据呢?软件开发中唯一真正有效的答案也适用于这里:

这取决于。

——每个开发者,总是

以下是 3+1 种数据转换方法,以及它们各自的优缺点:

0. 在后端

如果条件允许,这是我最喜欢的方案。如果后端返回的数据结构完全符合我们的需求,我们就无需做任何操作。虽然这在很多情况下听起来不太现实,例如在使用公共 REST API 时,但在企业级应用中也完全可以实现。如果您能够掌控后端,并且有一个端点能够返回符合您特定用例的数据,那么最好按照您期望的方式交付数据。

🟢 前端工作暂停
🔴 并非总是可行

1. 在 queryFn 中

queryFn是你传递给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)
Enter fullscreen mode Exit fullscreen mode

在前端,您可以像处理后端直接返回的数据一样处理这些数据。但您的代码中不会直接使用非大写的待办事项名称,也无法访问原始数据结构。如果您查看 react-query-devtools,会看到转换后的数据结构;如果您查看网络跟踪,则会看到原始数据结构。这一点可能令人困惑,请记住。

此外,react-query 在这里无法为您进行任何优化。每次执行数据获取操作时,您的转换都会运行。如果开销过大,请考虑其他替代方案。一些公司还提供共享的 API 层来抽象数据获取过程,因此您可能无法访问该层来进行数据转换。

🟢 在位置上非常“靠近后端”
🟡 转换后的结构最终会进入缓存,因此您无法访问原始结构
🔴 每次获取数据时都会运行
🔴 如果您有一个无法自由修改的共享 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()),
    }
}
Enter fullscreen mode Exit fullscreen mode

目前来看,这不仅会在每次执行 fetch 函数时运行,实际上还会在每次渲染时运行(即使是那些不涉及数据获取的渲染)。这很可能不是问题,但如果确实是问题,你可以使用useMemo进行优化。务必尽可能缩小依赖项的范围。queryInfodata内部的内容在引用上是稳定的,除非某些内容发生了真正的变化(在这种情况下,你需要重新计算转换),但queryInfo实际的内容则不然。如果你将queryInfo添加为依赖项,转换将再次在每次渲染时运行:

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

    return {
        ...queryInfo,
        // 🚨 don't do this - the useMemo does nothig 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,
        ]),
    }
}
Enter fullscreen mode Exit fullscreen mode

尤其当你的自定义钩子中包含需要与数据转换结合使用的额外逻辑时,这是一个不错的选择。请注意,数据可能未定义,因此在处理数据时请使用可选链式调用。

🟢 可通过 useMemo 进行优化
🟡 无法在开发者工具中查看确切结构
🔴 语法略显复杂
🔴 数据可能未定义

3. 使用选择选项

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

export const useTodosQuery = () =>
    useQuery(['todos'], fetchTodos, {
        select: (data) => data.map((todo) => todo.name.toUpperCase()),
    })
Enter fullscreen mode Exit fullscreen mode

选择器只有在数据存在时才会被调用,所以你不必担心这里是否存在 undefined。像上面这样的选择器也会在每次渲染时运行,因为函数标识会改变(它是一个内联函数)。如果你的转换开销很大,你可以使用 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()),
            []
        ),
    })
Enter fullscreen mode Exit fullscreen mode

此外,还可以使用选择选项仅订阅部分数据。这正是此方法的独特之处。请看以下示例:

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))
Enter fullscreen mode Exit fullscreen mode

在这里,我们通过向useTodosQuery传递自定义选择器,创建了一个类似useSelector的 API 。自定义钩子仍然像以前一样工作,因为如果不传递选择器,select将为undefined ,因此将返回整个状态。

但是,如果您传递一个选择器,那么您现在只会订阅选择器函数的结果。这非常强大,因为这意味着即使我们更新了待办事项的名称,仅通过`useTodosCount`订阅计数的组件也不会重新渲染。计数没有改变,因此 `react-query` 可以选择通知此观察器更新 🥳(请注意,这里做了一些简化,技术上并不完全正确——我将在第三部分更详细地讨论渲染优化)。

🟢最佳优化
🟢支持部分订阅
🟡每个观察者的结构可以不同
🟡结构共享执行两次(我将在第3部分更详细地讨论这一点)


今天就分享到这里啦👋。如果有什么问题,欢迎在推特上联系我
,或者在下方留言⬇️

文章来源:https://dev.to/tkdodo/react-query-data-transformations-geg