React Query笔记

786 阅读8分钟

什么是 React Query?

React Query 是一个 React 应用程序中的数据管理库。它提供了一种优雅的方式来处理数据获取、缓存、失效、重试以及许多其他数据管理方面的问题。使用 React Query,可以通过声明式 API 轻松地处理数据获取和状态管理。React Query 将数据获取和状态管理任务转移到后台,并提供了一个简单的 API 来处理缓存、重试和错误处理。

React Query 有许多功能,包括

  • 数据自动缓存和失效
  • 自动请求重试
  • 基于 Promise 的异步查询
  • 分页和无限滚动支持
  • 与 React Suspense 集成

React Query 还提供了许多插件,可以扩展其功能,例如

  • react-query/devtools:提供一个 React DevTools 面板,用于查看 React Query 的缓存和请求。
  • react-query/hydration:在 SSR 应用程序中自动提取数据并在客户端上进行缓存。
  • react-query/persistCache:将缓存存储在本地存储中,以便在刷新后重新加载。

React Query 的开发人员 TanStack 还提供了许多其他有用的 React 库,例如 React Router 和 Formik。这些库的一致性和互操作性使它们成为构建高质量 React 应用程序的理想选择。

安装和设置

首先,您需要在应用程序中安装 React Query。您可以通过 npm 或 yarn 安装 React Query:

npm install react-query
yarn add react-query

在您的应用程序中导入 React Query:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

然后,创建一个queryClient 并将其传递给您的应用程序:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0, // 失败重试
      retryDelay: (attemptIndex) => Math.min(1000 * 3 ** attemptIndex, 30 * 1000), // 每次重试延迟以3的倍数递增(从1000ms开始),但不超过 30 秒
      refetchInterval:number | false| ((data: TData | undefined, query: Query) => number | false)// 设置为数字时开启轮询
      refetchOnWindowFocus: false, // 窗口获取焦点重新获取
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app components */}
    </QueryClientProvider>
  );
}
  • refetchOnReconnect:重新连接时如果数据过期,是否重新查询获取,默认true
  • refetchOnMount:挂载时如果数据过期,是否重新查询,默认true
  • 更多配置参考官方文档,上述配置可在全局配置,也可用于单独请求

现在,您已经准备好开始使用 React Query!

基本用法

React Query 的核心是一个名为 useQuery 的 React Hook。可以通过 useQuery 来发起数据请求,并将返回值作为组件的渲染结果。例如,下面的示例使用  useQuery 获取一个名为 todos 的任务列表:

import { useQuery } from '@tanstack/react-query';

function TodoList() {
  const { isLoading, error, data } = useQuery('todos', () =>
    fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json())
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading todos: {error.message}</div>;

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

在这个例子中, useQuery 接收两个参数:查询键和一个异步回调函数。查询键是一个字符串,用于标识查询结果。异步回调函数会在首次渲染时被调用,并返回一个 Promise,该 Promise 将在数据加载完成后解析为查询结果。当数据正在加载时, isLoading 将为 true;当加载完成时, data 将包含查询结果;如果发生错误,则 error 将包含一个错误对象。

1. useQuery

用于查询信息请求,例如获取页面信息,登录时候等自动发起请求。

  import { useQuery } from '@tanstack/react-query';
  
  const { isLoading, data, refetch } = useQuery<TData>({
    queryKey: ['/requestPath', id, dependentQuery], // 必填,请求键,如果请求需要携带参数,参数需要放在数组中
    queryFn: () => request('/requestPath', { id: id }), // 必填,请求函数
    onSuccess: (data, param) => {}, // 选填,成功后执行的函数
    onError: (data, param) => {}, // 选填,失败后执行的函数
    select: (data) => {}, // 选填,可用于转换或选择查询函数返回的部分数据。它影响返回的数据值,但不影响存储在查询缓存中的内容。
    enabled: !id, // 选填,是否可用,设置为false之后只能通过refetch手动触发
  });

1.1. 返回结果

  • refetch:用于手动触发请求
  • data:请求返回数据,响应式渲染

1.2. queryKey

查询键必须是一个数组,可以简单到只有一个字符串的数组,也可以复杂到包含许多字符串和嵌套对象的数组。只要查询键是可序列化的,并且对查询的数据是唯一的,就可以使用。

  • 查询键可以包含字符串,也可以包含变量,作为该查询的唯一标识
  • 查询键变量改变时,会自动重新发起新一次请求。
  • 查询键中是否包含请求路径不会影响该次请求,但包含请求路径后面有妙用~

1.3. queryFn

查询函数可以是任何返回Promise的函数

你可以在React Query的文档中找到更多关于useQuery的信息和用法示例

2. useMutation

当你想要在React应用程序中使用Mutation时,可以使用React Query提供的useMutation hook。这个hook可以帮助你轻松地执行Mutation操作并更新相关的状态,属性同useQuery

使用useMutation hook来执行Mutation并更新状态。例如:

import { useMutation } from '@tanstack/react-query';
  
function TodoList() {
  const { data, isLoading, mutate } = useMutation({
    mutationFn: (...param: any) => {
      return request('/requestPath', { ...param });
    },
    onSuccess: () => {
  		message.success('success');
    },
    onError: () => {}
  });

  return <Button onClick={() => mutate({ id: 1, name: 'John' })}>{isLoading ? 'Saving...' : 'Save'}</Button>;
}

你可以在React Query的文档中找到更多关于useMutation的信息和用法示例

特性

1. 缓存和失效

React Query 会自动缓存查询结果,并在必要时自动失效。例如,如果在后续渲染中再次调用 useQuery('todos'),React Query 将返回缓存的结果,而不是重新发起请求。当数据过期时,React Query 会在后台自动重新获取数据,并更新缓存。这意味着可以在没有任何额外代码的情况下实现数据缓存和自动失效。

可以使用 useQuery 的 staleTime 选项来设置查询结果的过期时间。例如,下面的示例设置 staleTime 为 10 分钟:

const { isLoading, error, data } = useQuery('todos', () =>
  fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
  { staleTime: 1000 * 60 * 10 } // 10 minutes
);

当查询结果过期时,React Query 将在后台重新获取数据,并在完成后更新缓存。如果需要手动刷新数据,可以使用 useQuery 的 refetch 方法。

2. 请求重试

React Query 会自动重试请求,直到成功为止。可以使用 useQuery 的 retry 选项来设置重试次数和重试间隔。例如,下面的示例设置 retry 选项为 3 次重试,每次重试间隔 5 秒:

const { isLoading, error, data } = useQuery('todos', () =>
  fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
  { retry: 10 }, // 最多重试10次 
);

3. 分页和无限滚动

React Query 支持分页和无限滚动。可以使用 useInfiniteQuery 和 usePaginatedQuery 来处理分页和无限滚动的数据获取。例如,下面的示例使用 useInfiniteQuery 获取一个无限滚动的任务列表:

3.1 useInfiniteQuery


import { useInfiniteQuery } from '@tanstack/react-query';
import { useState } from 'react';

const fetchUsers = async ({ pageParam = 1 }) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users?_page=${pageParam}&_limit=10`);

  if (!response.ok) {
    throw new Error('Failed to fetch users');
  }

  const data = await response.json();

  return {
    data,
    nextPage: data.length > 0 ? pageParam + 1 : null
  };
};

const Users = () => {
  const [page, setPage] = useState(1);

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isFetching,
  } = useInfiniteQuery('users', fetchUsers, {
    getNextPageParam: (lastPage) => lastPage.nextPage,
    onSuccess: (data) => {
      console.log('Data loaded', data);
    },
    onError: (error) => {
      console.error('Error fetching data', error);
    },
    onSettled: () => {
      console.log('Query completed');
    }
  });

  const handleLoadMore = () => {
    fetchNextPage();
    setPage(page + 1);
  };

  return (
    <div>
      {error && <div>Error fetching users: {error.message}</div>}

      {data && (
        <ul>
          {data.pages.map((page) =>
            page.data.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))
          )}
        </ul>
      )}

      {isFetching && <div>Loading users...</div>}

      {hasNextPage && (
        <button
          onClick={handleLoadMore}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading more...' : 'Load more'}
        </button>
      )}
    </div>
  );
};

export default Users;

3.2 usePaginatedQuery

import { usePaginatedQuery } from '@tanstack/react-query';

const fetchUsers = async (key, page = 1) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users?_page=${page}&_limit=10`);

  if (!response.ok) {
    throw new Error('Failed to fetch users');
  }

  const data = await response.json();

  return data;
};

const Users = () => {
  const {
    resolvedData,
    latestData,
    status,
    error,
    isFetching,
    isFetchingMore,
    canFetchMore,
    fetchMore,
  } = usePaginatedQuery(['users', 1], fetchUsers);

  const handleLoadMore = () => {
    fetchMore();
  };

  return (
    <div>
      {status === 'error' && <div>Error fetching users: {error.message}</div>}

      {status === 'loading' && <div>Loading users...</div>}

      {status === 'success' && (
        <>
          <ul>
            {resolvedData.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>

          <button
            onClick={handleLoadMore}
            disabled={!canFetchMore || isFetchingMore}
          >
            {isFetchingMore ? 'Loading more...' : canFetchMore ? 'Load more' : 'Nothing more to load'}
          </button>
        </>
      )}

      {isFetching && <div>Fetching more users...</div>}
    </div>
  );
};

export default Users;

3.3 usePaginatedQuery和useInfiniteQuery区别

  1. usePaginatedQuery

usePaginatedQuery用于获取指定页的数据,通常用于在一个固定的页面上进行分页。当你需要在特定页面上显示特定数量的数据时,可以使用usePaginatedQuery

使用usePaginatedQuery时,你需要指定初始页面和每页的数据数量,然后在需要时使用fetchMore来获取更多的数据。每次调用fetchMore时,它将使用上一次查询的参数来获取下一页的数据。这种方式更适合于静态数据,例如电子商务网站上的商品列表。

  1. useInfiniteQuery

useInfiniteQuery用于获取无限滚动的数据,通常用于在一个无限滚动的列表上进行分页。当你需要在用户向下滚动页面时动态加载数据时,可以使用useInfiniteQuery

使用useInfiniteQuery时,你可以指定查询函数和获取下一页数据所需的参数。React Query将自动管理分页数据和缓存,并提供了fetchNextPage函数来获取下一页的数据。这种方式更适合于动态数据,例如社交媒体网站上的帖子列表。

综上所述,usePaginatedQueryuseInfiniteQuery都可以用于分页查询,但它们适用于不同的场景。如果你需要在固定页面上显示特定数量的数据,请使用usePaginatedQuery。如果你需要在无限滚动的列表上动态加载数据,请使用useInfiniteQuery

4. queryKey的妙用

QueryClient可以用来与缓存交互,例如清空缓存,发起已声明的请求等。

开头我们用查询客户端将我们的应用包裹起来,即可以通过创建查询客户端获取应用下全部已声明的请求

import { useQueryClient } from '@tanstack/react-query'; 
const queryClient = useQueryClient(); 
queryClient.invalidateQueries(['/requestPath']);

invalidateQueries:可根据查询键使缓存中的单个或多个查询无效和重新获取。

示例:

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

function UserList() {
  const { data: userList, isLoading } = useQuery({
    queryKey: ['/user/userList'],
    queryFn: () => _getUserList()
  });

  return userList?.map((user, index) => <div key={index}>{user.name}</div>);
}


function UpdateUser() {
  const queryClient = useQueryClient();
  queryClient.invalidateQueries(['/requestPath']);
  const { mutate: updateUser } = useMutation({
    mutationFn: (data) => request('/user/updateUser', data),
    onSuccess: () => {
      message.success('success');
      queryClient.invalidateQueries(['/user/userList']);
    }
  });

  return (
    <Button
      onClick={() => {
        updateUser({ name: 'tom', id: 222 });
      }}
    >
     Update User
    </Button>
  );
}

5. 轮询

import { useQuery } from '@tanstack/react-query'; 

// remove 可结束轮询请求
const { remove } = useQuery(
  ['/queryJob', jobId],// jobId变化时,自动发起新轮询请求
  () => _queryJob({ jobId }),
  {
    enabled: !!jobId,// 轮询开启条件
    refetchInterval: (data) => {
      // 判断是否轮询的条件 返回false不轮询接口
      if (data.status === 1) {
        return 15 * 1000;// 轮询间隔
      }
      return false;
    },
    onSuccess: () => {
      // success
    }
  }
);

总结

React Query 是一个用于 React 应用程序的数据管理库,它提供了许多有用的功能,如数据缓存、懒加载、轮询、预取等,使得数据管理变得更加轻松和高效。使用 React Query 可以大大提高应用程序的性能和稳定性,减少代码量,提高开发效率。