一、简介

react-query 是一个基于Hooks的React库,主要用于管理异步数据获取、缓存和状态更新。它通过提供一系列方法来处理数据获取、缓存、失效、重试以及其他数据管理方面的问题。使用声明式API轻松地处理数据获取和状态管理。React Query将数据获取和状态管理任务转移到后台,并提供了一个简单地API来处理 缓存、重试和错误处理

以前叫 React query,后面开始支持vue等其他前端框架,改名叫 TanStack Query(@tanstack/react-query)

二、功能

数据管理功能
  • 数据获取

    • 自动缓存:数据会被自动缓存,避免重复请求。

    • 加载状态:提供 isLoading 和 isFetching 状态,方便 UI 渲染。

    • 错误处理:提供 isError 和 error 状态,便于错误处理。

    • 依赖查询:支持根据条件动态获取数据。

  • 数据缓存

    • 缓存过期时间:通过 staleTime 和 cacheTime 控制缓存的有效期。

    • 后台刷新:在数据过期时自动在后台刷新。

    • 缓存共享:多个组件共享同一缓存,避免重复请求。

  • 数据更新

    • 乐观更新:在请求完成前更新 UI,提升用户体验。

    • 自动失效:在数据更新后,自动使相关缓存失效并重新获取数据。

    • 副作用处理:通过 onSuccessonError 等回调处理副作用。

  • 数据同步

    • 窗口聚焦刷新:在用户重新聚焦窗口时自动刷新数据。

    • 网络重连刷新:在网络重新连接时自动刷新数据。

    • 轮询:通过 refetchInterval 定期刷新数据。

  • 数据清理

    • 窗口聚焦刷新:在用户重新聚焦窗口时自动刷新数据。

    • 网络重连刷新:在网络重新连接时自动刷新数据。

    • 轮询:通过 refetchInterval 定期刷新数据。

  • 数据状态管理

    • 加载状态:isLoadingisFetching

    • 错误状态:isErrorerror

    • 数据状态:data

    • 乐观更新状态:通过 setQueryData 手动更新缓存。

数据展示和交互功能
  • 分页和无限滚动支持

    • React Query支持分页和无限滚动的数据获取,适用于需要展示大量数据的场景。

  • 与React Suspense集成

    • 通过设置useQuery钩子的suspense选项为true,可以与React Suspense集成,实现更优雅的异步数据加载和错误处理。

插件和扩展功能
  • react-query/devtools

    • 提供了一个React DevTools面板,用于查看和管理React Query的缓存、请求历史和查询状态。这有助于开发者在开发过程中更高效地调试和优化应用。

  • react-query/hydration

    • 在服务器端渲染(SSR)应用程序中,可以自动提取数据并在客户端上进行缓存。这有助于减少客户端的加载时间,提升用户体验。

  • react-query/persistCache

    • 可以将缓存存储在本地存储中,以便在刷新页面后重新加载数据。这有助于保持应用的状态一致性,提升用户体验。

三、使用

安装和使用

使用react-query,可以通过npm或yarn进行安装:

npm install react-query
# 或者
yarn add react-query

在React应用中,通常需要创建一个QueryClient实例,并通过QueryClientProvider将其传递给应用的根组件:

javascriptCopy Codeimport { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();
// 根组件
function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

// 其他组件使用时
const queryClient =  useQuery({ queryKey: ['todos'], queryFn: getTodos })

这样,整个应用中的所有组件都可以通过useQueryuseMutation等Hooks来访问和管理数据。

useQueryClient()

它允许你在组件中获取当前的 QueryClient 实例。QueryClient 是 React Query 的核心,负责管理查询的缓存、状态以及触发查询等操作。通过 useQueryClient,你可以在组件中方便地访问这些功能。

  • QueryClient实例的方法

    • fetchQuery:手动触发一个查询,并返回查询结果。如果缓存中有对应的数据且未过期,则可以直接使用缓存数据;否则,会重新请求数据并缓存。

    • invalidateQueries:标记查询为过期,下次使用时重新获取,是“懒惰”更新。

    • refetchQueries:立即强制重新获取查询数据,是“主动”更新。

    • getQueryData:从缓存中获取指定查询的数据。

    • setQueryData:手动设置指定查询的缓存数据。

    • resetQueries:将指定的查询重置为初始状态。如果查询具有 initialData,则数据将被重置为该数据。

    • removeQueries:从缓存中删除指定的查询。

    • cancelQueries:取消指定的查询。

  • 注意事项

    • 确保 QueryClientProvider 包裹

    • 避免频繁调用

    • 理解查询状态和生命周期(isLoadingisSuccessisError 等状态)

PS:可以在创建的时候传默认配置,比如staleTimecacheTime 和 refetchOnWindowFocus,可以避免在每个 useQuery 调用中重复设置相同的选项。

useQuery({queryKey,queryFn,...queryOptions })

是 React Query 库中的一个钩子(Hook),用于从服务器获取数据,并在 React 组件中展示这些数据。它简化了数据获取、缓存、更新和错误处理的流程,使得开发者能够更加专注于业务逻辑的实现。

queryKey【查询key】
  • 是一个数组,可序列化

    • 简单形式

      • queryKey可以由一个字符串组成,用于标识基本的查询。例如:useQuery({ queryKey: ['userData'], ... })

    • 复杂形式

      • 当查询需要更多的信息来唯一描述数据时,queryKey可以包含多个字符串或可序列化对象。例如,根据userId查询用户信息时,可以使用:useQuery({ queryKey: ['userInfo', userId], ... })

  • 是标识查询的唯一值

  • 当queryKey是变量时,构成了查询依赖,只要变量值改变就会触发查询,类似于useEffect中的依赖项

  • 使用场景

    • 数据缓存:通过使用queryKey,React Query能够自动缓存查询结果。当相同的查询(即queryKey相同)再次发生时,React Query会直接使用缓存中的数据,从而提高性能。

    • 数据更新:当查询条件发生变化时(例如分页、排序、过滤等),可以通过改变queryKey来触发新的查询。React Query会根据新的queryKey重新向服务器发送请求,并更新缓存数据。

    • 数据同步:React Query支持自动或手动更新数据。当后台数据发生变化时,可以通过调用相关方法(如invalidateQueries)来使匹配的查询失效,并触发新的数据获取和缓存更新。

// 当前页码,改变页码,请求就会更新
const [page, setPage] = React.useState(0)
// 请求函数
  const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json())

  const {
    isLoading,
    isError,
    error,
    data,
    isFetching,
    isPreviousData,
  } = useQuery({
    queryKey: ['projects', page],
    queryFn: () => fetchProjects(page),
    // 使用这个配置,可以防止切换页请求时,频繁的`loading`切换
    keepPreviousData : true //在获取到新数据之前保持原有数据,优化用户体验
  })

queryFn【查询函数】
  • queryFn就是一个普通的异步函数(promise函数)

  • 返回的值会作为data放入,默认会给这个函数注入一些参数

  • resolve就会正常返回,thorw错误就会导致status = error 

queryOptions 
  • 缓存和重新获取

    • enabled

      • boolean -- 默认true

      • 如果设置为 false,查询将不会执行,也不会从缓存中获取数据

大部分情况都没必要设置false,请求需要后续的某个操作触发,一定有变量变化,监听这个变化就行了

  • retry

    • number | false | RetryFunction -- 默认3,重试3次

    • 如果查询失败,是否自动重试,以及重试的次数或自定义的重试逻辑

  • refetchOnMount

    • boolean -- 默认true

    • 组件挂载时是否自动重新获取数据

  • refetchOnWindowFocus

    • boolean -- 默认true

    • 当用户切换回包含该组件的窗口或标签页时,是否自动重新获取数据

  • cacheTime

    • number -- 默认Infinity

    • 查询结果在缓存中保留的时间

  • 更新和轮询

    • staleTime

      • number -- 默认0

      • 查询结果的staleTime过期时,React Query会将其标记为陈旧,并在后台尝试重新获取数据。然而,用户看到的仍然是旧数据,直到新数据到达并被更新到UI上。

    • pollingInterval

      • number -- 默认0

      • 轮询间隔时间

  • 错误处理

    • onError 回调函数

  • 其他选项

    • select 

      • (data: any) => any

      • 用于在查询成功后,对从服务器获取的数据进行转换或处理的函数。允许你在将数据传递给组件之前,对其进行格式化、过滤或映射等操作。select 只有在 data 存在的时候才会被调用,不用担心它是 undefined

    • placeholderData

      • 查询时展示的临时数据的对象

    • initialData

      • 初始展示的数据对象,和placeholderData的区别是,他会被缓存

    • notifyOnChange

      • boolean | 'always' | ((prevData: any, nextData: any) => boolean),对于引用类型的变化是 true,对于原始类型的变化是 false

      • 控制是否在数据变化时通知客户端

    • keepPreviousData

      • boolean -- 默认true

      • 如果查询失败,是否保留上一次成功查询的结果

并发请求

React.Suspense 主要用于处理组件的异步加载,通常与动态导入(React.lazy)结合使用来代码分割和按需加载组件。然而,React.Suspense 并不直接支持或管理多个并发请求的加载状态。

function App () {
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
  const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
}
串行请求

有的请求需要前一个请求完成后再请求,或者需要等到某个数据有值后再请求,就可以使用enabled配置控制

// 先请求用户
const user = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})
const userId = user.data?.id

// 再请求项目
const projects = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // 当userId存在的时候才会请求
  enabled: !!userId, 
})

请求状态
  • fetchStatus 绑定请求函数的状态

    • fetching -- isFetching

    • pause -- isPause

    • idle -- isIdle

  • status绑定请求数据状态

    • loading -- isLoading

    • error -- isError

    • success -- isSuccess

isFetching?isLoading?

isFetching表示是否正在后台刷新数据,loading表示是否在后台获取数据

  • refetch

    • 类型:() => Promise<void>

    • 描述:一个函数,调用它会重新触发查询,并从服务器获取新数据。

  • remove

    • 类型:() => void

    • 描述:一个函数,调用它会从缓存中移除该查询及其数据。

useMutation({mutationFn:()=>{}}) 

useMutation 是 React Query 提供的一个钩子,用于处理数据变更操作(如创建、更新、删除等)。与 useQuery 不同,useMutation 不会自动执行,而是需要手动触发。它非常适合处理表单提交、数据更新等场景。

用法

  • 定义突变函数

    • 数据变更操作异步函数

  • 使用 useMutation

    • 使用 useMutation 钩子,并传入这个异步函数。useMutation 将返回一个对象,该对象包含多个属性和方法,用于处理突变的状态和副作用。

  • 触发突变

    • 通过调用 mutate 方法来触发数据变更操作。你可以传递任何需要的参数给 mutate 方法,这些参数将被传递给你的异步函数。

  • 处理状态和错误

    • useMutation 返回的对象包含状态信息,如 isPending(是否正在加载)、isError(是否发生错误)以及 error(错误信息)。你可以使用这些信息来向用户显示加载状态、错误消息等。

  • 重置和清理

    • 你可以使用 reset 方法来重置突变的状态,或者在组件卸载时使用 onSettled 或 onError 等回调来执行清理操作。

...


const createPost = async (newPost) => {
 // 异步请求返回数据
};

const MutationDemo = () => {

  // 使用 useMutation 处理数据创建
  const mutation = useMutation(createPost, {
    onSuccess: (data) => {
      // 数据获取成功
      // 进行数据更新操作...
    },
    onError: (error) => {
      // 数据获取失败
    },
    onMutate:()=>{
      // 乐观更新
      // 适用场景:实时性要求高的应用,高交互性应用...
    }
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    // 触发 mutation
    mutation.mutate({ params });

    // mutation包含数据触发突变的方法和状态,例如isLoading、isError
  };

 // 渲染,操作触发 handleSubmit-->突变
};

useInfiniteQuery({queryKey,queryFn,...queryOptions }) 【无限请求、滚动加载】

结合InfiniteScroll from "react-infinite-scroll-component";可以实现无限滚动查询

  • queryKey: 查询的唯一标识符。

  • queryFn

    • 异步函数,用于获取数据。这个函数可以接受一个参数pageParam,表示当前页码。

  • initialPageParam 获取第一页时要使用的默认页面参数

  • getNextPageParam

    • 当接收到该查询的新数据时,该函数将接收无限数据列表的最后一页和所有页面的完整数组,以及pageParam信息。

    • 它应该返回一个变量,该变量将作为最后一个可选参数传递给查询函数。

    • 返回undefined或null表示没有可用的下一页。

  • queryOptions 

    • getPreviousPageParam: 一个函数,用于从第一页的数据中获取上一页的 pageParam

    • onSuccess, onError 等

返回的data中有什么数据?

  • pages

    • 类型: Array

    • 包含所有已加载的页面数据。

[
  { data: [item1, item2], nextPage: 2 }, // 第一页
  { data: [item3, item4], nextPage: 3 }, // 第二页
  { data: [item5, item6], nextPage: 4 }, // 第三页
]
  • pageParams

    • Array

    • 包含每页的 pageParam 值。[1, 2, 3] 

  • isFetchingNextPage: boolean

    • 类型: boolean

    • 表示是否正在加载下一页数据。

    • 当 fetchNextPage 被调用时,isFetchingNextPage 会变为 true,直到数据加载完成。

  • isFetchingPreviousPage: boolean

import InfiniteScroll from 'react-infinite-scroll-component';
import { useInfiniteQuery } from 'react-query';

// 1. 定义获取数据的函数
function fetchData({ pageParam = 1 }) {
  ...
}

function InfiniteScrollComponent() {
  // 2. 使用 useInfiniteQuery 获取分页数据
  const {
    data,           // 所有页面的数据
    fetchNextPage,  // 加载下一页的函数
    hasNextPage,    // 是否还有更多数据
    isFetching,     // 是否正在加载
  } = useInfiniteQuery('data', fetchData, {
    getNextPageParam: (lastPage) => lastPage.nextPage, // 获取下一页的页码
  });

  // 3. 将所有页面的数据扁平化
  const allData = data ? data.pages.flatMap(page => page.items) : [];

  return (
    <div>
      {/* 4. 使用 InfiniteScroll 组件 */}
      <InfiniteScroll
        dataLength={allData.length} // 当前已加载的数据长度
        next={fetchNextPage}       // 加载更多数据的函数
        hasMore={hasNextPage}      // 是否还有更多数据
        loader={<div>Loading...</div>} // 加载中的提示
        endMessage={<div>No more data!</div>} // 数据加载完毕的提示
      >
        {/* 5. 渲染数据 */}
        <ul>
          {allData.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      </InfiniteScroll>

      {/* 6. 加载状态反馈 */}
      {isFetching && <div>Loading initial data...</div>}
    </div>
  );
}

//useInfiniteQuery 返回的数据是按页存储的,结构类似下面的
//滚动加载中,通常需要将所有数据合并为一个
{
  pages: [
    { data: [item1, item2, item3], nextPage: 2 }, // 第一页
    { data: [item4, item5, item6], nextPage: 3 }, // 第二页
    { data: [item7, item8, item9], nextPage: 4 }, // 第三页
  ],
  pageParams: [1, 2, 3], // 每页的参数
}


queryClient.prefetchQuery
  • 作用:

    • prefetchQuery 允许在实际需要数据之前预先获取数据并缓存。

    • 这可以显著减少等待时间,提高应用程序的响应速度。

  • 实用性:

    • 在用户可能需要查看某个页面或组件之前,预先获取数据。

    • 在用户将鼠标悬停在链接上时,预先获取链接指向页面的数据。

四、demo

import React, { useState } from 'react';
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

// 模拟 API 调用
const fetchTodos = async () => {
  return [
    { id: 1, text: 'Todo 1', completed: false },
    { id: 2, text: 'Todo 2', completed: false },
    { id: 3, text: 'Todo 3', completed: true },
  ];
};

const updateTodo = async (todoId, updatedTodo) => {
  // 这里可以添加实际的 API 请求逻辑
  return updatedTodo;
};

const addTodo = async (newTodo) => {
  // 生成一个新的 ID(在实际应用中,通常从后端获取)
  const id = Math.max(...(await fetchTodos()).map(todo => todo.id)) + 1;
  const newTodoWithId = { ...newTodo, id };
  // 这里可以添加实际的 API 请求逻辑
  return newTodoWithId;
};

const deleteTodo = async (todoId) => {
  // 这里可以添加实际的 API 请求逻辑
  return true;
};

const TodoList = () => {
  const queryClient = useQueryClient();
  const { isLoading, error, data: todos } = useQuery(['todos'], fetchTodos);

  const addTodoMutation = useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries(['todos']);
    },
  });

  const updateTodoMutation = useMutation(updateTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries(['todos']);
    },
  });

  const deleteTodoMutation = useMutation(deleteTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries(['todos']);
    },
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return (
    <div>
      <h1>Todo List</h1>
      <input
        type="text"
        placeholder="Add new todo"
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            addTodoMutation.mutate({ text: e.target.value, completed: false });
            e.target.value = '';
          }
        }}
      />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={(e) => {
                updateTodoMutation.mutate({ ...todo, completed: e.target.checked }, {
                  select: (updatedTodo) => updatedTodo,
                });
              }}
            />
            {todo.text}
            <button onClick={() => deleteTodoMutation.mutate(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  );
}

export default App;

[警告]&[问号]

在使用 React Query 的 useQuery 时,select 选项用于对查询返回的数据进行转换或处理。它允许你在数据返回后,对数据进行格式化、过滤或其他操作。然而,当你使用 setQueryData 手动更新缓存数据时,需要特别注意 数据格式的统一性,手动将数据转换为与 select 处理后的格式一致。

  • select 用于对查询返回的数据进行处理,返回一个新的数据结构。select 只有在 data 存在时才会触发。如果查询尚未完成或数据不存在,select 不会被调用。因此,你不需要在 select 函数中额外处理 data 不存在的情况。

  • setQueryData 用于手动更新缓存中的数据。它直接修改缓存中的数据,不会触发 select 逻辑。

isSuccess 表示查询成功完成,但并不保证 data 立即包含最新的数据。在某些情况下,由于网络延迟或缓存机制,data 可能需要一些时间才能更新如果您需要确保数据是最新的,

  • 可以使用 isFetching 状态。表示查询正在获取数据,包括初始获取和后台重新获取。您可以在 isFetching 为 false 且 isSuccess 为 true 时执行数据处理逻辑。

  • 添加 data 数据的判断

enable,变为true,useQuery 会立即发起网络请求,获取数据。enable,变为false,会停止正在进行的网络请求(如果存在),查询数据会被缓存,但不会再进行后台更新。

在 TanStack Query(以前称为 React Query)中,当 queryKey 包含对象或数组时,默认情况下会进行浅比较。这意味着:

  • 引用地址比较:

    • TanStack Query 会比较对象或数组的引用地址,而不是它们的内容。

    • 如果对象或数组的内容发生变化,但引用地址保持不变,TanStack Query 不会检测到 queryKey 的变化。

  • 不会触发查询:

    • 因此,如果queryKey 包含一个对象,并且该对象内部的值发生了变化,但对象的引用地址没有改变,那么 TanStack Query 不会触发查询的重新获取。

  const [filters, setFilters] = useState({ status: 'active' });

  const { data, isLoading } = useQuery({
    queryKey: ['tasks', filters],
    queryFn: () => fetchTasks(filters), 
  });

  const updateFilters = () => {
    
    filters.status = 'completed';
    setFilters({ ...filters }); 
  };
// 由于我们直接修改了 filters 对象内部的值,它的引用地址并没有改变。也不会触发 fetchTasks 函数
// 但是如果使用setFilters({ ...filters }); 这样就可以生成新的对象,引用地址会发生变化,从而触发查询。
// 可以使用扩展运算符(...)或 Object.assign() 来创建新的对象。
// 如果对象中的字段很多,或者嵌套很深,可以使用JSON.stringify()将对象序列化为字符串
// 可以使用 useMemo() 来缓存对象或数组,以避免不必要的重新创建

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐