@tanstack/react-query详解 🔥🔥🔥React的异步数据管理神器

什么是 React Query

React Query 是一个用于管理 React 应用中异步数据的库,主要用于处理网络请求、数据缓存、数据同步等问题。它提供了一种简单而强大的方式来管理应用中的数据获取、缓存和更新,使开发者可以更专注于应用逻辑而非数据管理细节

解决了什么问题

  1. 简化数据管理React Query 提供了统一的 API 来处理数据获取、缓存和更新,避免了手动管理状态的复杂性。
  2. 自动缓存:默认会缓存请求结果,当数据变化时自动重新获取,减少不必要的网络请求。
  3. 数据同步 :当多个组件使用相同的 queryKey 时,React Query 会共享数据,确保数据一致性。
  4. 加载状态管理 :内置了 isPending、isLoading、isError 等状态,简化了加载和错误处理。
  5. 自动重新获取:在组件重新渲染、窗口重新聚焦等情况下自动重新获取数据。
  6. 数据预取:可以预取数据,提升用户体验。
  7. 优化网络请求:通过合并请求和缓存策略,减少不必要的网络请求。
  8. 易于测试:组件逻辑与数据获取解耦,使测试更加简单。

快速上手

安装

sql 复制代码
pnpm add @tanstack/react-query
pnpm add @tanstack/react-query-devtools // 开发调试工具,用于在开发环境中监控和调试应用中的数据查询状态,自行选择

配置

javascript 复制代码
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { RouterProvider } from "react-router-dom";
import router from "./routes";

// Create a client
const queryClient = new QueryClient();
function App() {
    return (
        <QueryClientProvider client={queryClient}>
            <ReactQueryDevtools /> // 开发调试工具,此组件默认只在开发环境生效,生产环境会自动移除,不会增加打包体积
            <RouterProvider router={router} />
        </QueryClientProvider>
    );
}

export default App;

两个主要的hooks

useQuery 用于获取数据

javascript 复制代码
import React, { memo } from "react";
import type { FC, ReactNode } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";

interface IProps {
    children?: ReactNode;
}
const QueryTest: FC<IProps> = () => {
    const { isPending, error, data, isFetching, refetch } = useQuery({
        queryKey: ["dataInfo"], // 唯一的查询键
        queryFn: async () => {
            const response = await fetch(
                "https://api.github.com/repos/TanStack/query"
            );
            return await response.json();
        }, // 查询函数
        staleTime: 5 * 60 * 1000, // 5分钟数据不过期
        retry: 2, // 重试次数
    });
    if (isPending) return "Loading...";

    if (error) return "An error has occurred: " + error.message;

    return (
        <div>
            <button
                onClick={() => {
                    refetch();
                }}
                className="bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-6 rounded-md transition duration-300 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
            >
                刷新数据
            </button>
            <div className="text-2xl font-bold text-gray-800 mb-4">组件1</div>
            <h1>{data.full_name}</h1>
            <p>{data.description}</p>
            <strong>👀 {data.subscribers_count}</strong>{" "}
            <strong>✨ {data.stargazers_count}</strong>{" "}
            <strong>🍴 {data.forks_count}</strong>
            <div>{isFetching ? "Updating..." : ""}</div>
        </div>
    );
};

export default memo(QueryTest);

useMutation 用于处理数据修改操作(创建、更新、删除)

javascript 复制代码
import { useMutation, useQueryClient } from 'react-query';

function CreateUserForm() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (userData) => {
      return fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      }).then(res => res.json());
    },
    
    // 成功回调
    onSuccess: (data, variables, context) => {
      console.log('创建成功:', data);
      // 使相关查询失效,触发重新获取数据
      queryClient.invalidateQueries(['users']);
    },
    
    // 错误回调
    onError: (error, variables, context) => {
      console.error('创建失败:', error);
      // 可以显示错误提示
      alert('创建用户失败: ' + error.message);
    },
    
    // 请求前回调
    onMutate: async (newUser) => {
      console.log('开始创建用户:', newUser);
      // 可以在这里进行乐观更新
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const userData = {
      name: formData.get('name'),
      email: formData.get('email')
    };
    
    // 执行 mutation
    mutation.mutate(userData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="姓名" required />
      <input name="email" placeholder="邮箱" required />
      
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '创建中...' : '创建用户'}
      </button>
      
      {mutation.isError && (
        <div style={{ color: 'red' }}>
          错误: {mutation.error.message}
        </div>
      )}
    </form>
  );
}

useMutation 返回的对象

useMutation 返回一个包含以下属性的对象:

javascript 复制代码
const mutation = useMutation({ mutationFn: createUser });

// mutation 对象包含以下属性:
const {
  data,           // 成功时返回的数据
  error,          // 错误对象
  isIdle,         // 尚未开始(初始状态)
  isLoading,      // 正在执行中
  isSuccess,      // 执行成功
  isError,        // 执行失败
  status,         // 状态字符串: 'idle' | 'loading' | 'success' | 'error'
  mutate,         // 触发执行的函数
  mutateAsync,    // 返回 Promise 的触发函数
  reset,          // 重置状态
  variables,      // 最后一次调用时传递的参数
  context,        // onMutate 返回的上下文
} = mutation;

配置完成就可以在组件里面进行使用了,下面我们通过一些例子,来看下 React Query 是如何帮我们解决的异步数据管理里面痛难点

React Query 如何解决服务器数据管理的 9 大痛点

1. 解决状态管理的复杂性

传统方式 vs React Query

javascript 复制代码
// ❌ 传统方式 - 手动管理多个状态
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [isSuccess, setIsSuccess] = useState(false);

// ✅ React Query - 一个钩子搞定所有状态
const { 
  data, 
  isLoading, 
  isError, 
  error, 
  isSuccess,
  isFetching,
  status
} = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

// 自动管理所有状态,无需手动设置

2. 解决缓存管理的挑战

自动缓存和重复请求去重

javascript 复制代码
// ✅ 多个组件使用相同查询键,只会发送一次请求
// ComponentA.jsx
function ComponentA() {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
  // 渲染用户列表...
}

// ComponentB.jsx  
function ComponentB() {
  const { data: users } = useQuery({
    queryKey: ['users'], // 相同查询键,使用缓存
    queryFn: fetchUsers,
  });
  // 渲染用户统计 - 不会重复请求!
}

// ✅ 智能缓存失效
const queryClient = useQueryClient();

// 添加用户后,使所有 users 查询失效
const mutation = useMutation({
  mutationFn: addUser,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] });
    // 所有使用 ['users'] 查询键的组件都会自动重新获取数据
  },
});

当两个组件同时请求相同数据时:

  • 第一个请求发起网络调用
  • 第二个请求直接使用第一个请求的缓存或等待结果
  • 只会发起一次实际的网络请求
javascript 复制代码
// React Query 内部机制
const cache = {
  '["users"]': {
    data: { name: 'John', email: 'john@example.com' },
    status: 'success',
    lastUpdated: 1640995200000
  }
};

3. 解决数据同步和一致性

跨组件数据自动同步

javascript 复制代码
// ✅ 所有组件共享同一份数据
function UserList() {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
  
  return users?.map(user => <div key={user.id}>{user.name}</div>);
}

function UserStats() {
  const { data: users } = useQuery({
    queryKey: ['users'], // 相同查询键,数据自动同步
    queryFn: fetchUsers,
  });
  
  return <div>总用户数: {users?.length}</div>;
}

// 当数据更新时,所有组件自动保持同步

4. 解决竞态条件

自动请求取消和最新数据保证

javascript 复制代码
function UserProfile({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  
  // ✅ React Query 自动处理:
  // - 当 userId 变化时,自动取消之前的请求
  // - 总是显示最新 userId 对应的数据
  // - 无需手动 isCancelled 逻辑
  
  return <div>{user?.name}</div>;
}

// 快速切换 userId: 1 -> 2 -> 3
// 只会显示 userId=3 的数据,自动取消 userId=1 和 2 的请求

5. 解决错误处理和重试逻辑

内置错误处理和重试机制

javascript 复制代码
const { data, error, isError } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  // ✅ 内置配置,无需手动实现
  retry: 3,                    // 自动重试3次
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  onError: (error) => {
    // 统一的错误处理
    console.error('获取用户失败:', error);
  },
  onSuccess: (data) => {
    // 成功回调
    console.log('获取用户成功:', data);
  },
});

// ✅ 还可以全局配置默认重试行为
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error) => {
        // 根据错误类型决定是否重试
        if (error.status === 404) return false; // 404 不重试
        return failureCount < 3; // 其他错误重试3次
      },
    },
  },
});

6. 解决后台同步和乐观更新

简化的乐观更新

javascript 复制代码
import { useQueryClient } from '@tanstack/react-query';

function OptimisticUpdateExample() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: updateUser,
    onMutate: async (newUserData) => {
      // 1. 取消进行中的查询,避免覆盖乐观更新
      await queryClient.cancelQueries({ queryKey: ['user', newUserData.id] });
      
      // 2. 保存前一个状态,用于错误时回滚
      const previousUser = queryClient.getQueryData(['user', newUserData.id]);
      
      // 3. 乐观更新:立即更新 UI,直接修改 React Query 内部缓存的数据
      queryClient.setQueryData(['user', newUserData.id], (old) => ({
        ...old,
        ...newUserData,
      }));
      
      // 4. 返回上下文,用于错误回滚
      return { previousUser };
    },
    onError: (error, newUserData, context) => {
      // 发生错误时回滚到前一个状态
      queryClient.setQueryData(['user', newUserData.id], context.previousUser);
      
      showNotification('更新失败,已恢复原状态', 'error');
    },
    onSettled: (data, error, newUserData) => {
      // 确保数据最终一致
      queryClient.invalidateQueries({ queryKey: ['user', newUserData.id] });
    },
  });

  const handleUpdate = (userData) => {
    mutation.mutate(userData);
  };
}

7. 解决分页和无限加载的复杂性

内置分页和无限加载

javascript 复制代码
// ✅ 分页查询
function UsersPaginated() {
  const [page, setPage] = useState(1);
  
  const { data, isLoading, isPreviousData } = useQuery({
    queryKey: ['users', page],
    queryFn: () => fetchUsers(page),
    keepPreviousData: true, // 保持上一页数据,避免闪烁
  });

  return (
    <div>
      {data?.users.map(user => <div key={user.id}>{user.name}</div>)}
      <button 
        onClick={() => setPage(old => Math.max(old - 1, 1))}
        disabled={page === 1}
      >
        上一页
      </button>
      <button
        onClick={() => setPage(old => (data?.hasMore ? old + 1 : old))}
        disabled={isPreviousData || !data?.hasMore}
      >
        下一页
      </button>
    </div>
  );
}

// ✅ 无限加载
function UsersInfinite() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['users', 'infinite'],
    queryFn: ({ pageParam = 1 }) => fetchUsers(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextPage,
  });

  return (
    <div>
      {data.pages.map((page, i) => (
        <div key={i}>
          {page.users.map(user => <div key={user.id}>{user.name}</div>)}
        </div>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? '加载中...' : '加载更多'}
      </button>
    </div>
  );
}

8. 解决性能优化问题

自动性能优化

javascript 复制代码
function DataComponent() {
  const { data } = useQuery({
    queryKey: ['expensive-data'],
    queryFn: fetchExpensiveData,
    staleTime: 5 * 60 * 1000, // 5分钟内不会重新获取
  });

  // ✅ React Query 自动处理:
  // - 结构化共享:只更新真正变化的数据
  // - 智能重渲染:只有数据变化时才重新渲染
  // - 窗口聚焦重新获取:确保数据新鲜但不过度请求
  
  return <ExpensiveComponent data={data} />;
}

// ✅ 数据预加载
function UserLink({ userId }) {
  const queryClient = useQueryClient();
  
  const prefetchUser = () => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
    });
  };
  
  return (
    <Link to={`/user/${userId}`} onMouseEnter={prefetchUser}>
      用户详情
    </Link>
  );
}

9. 解决开发体验和维护成本

统一的 API 和开发工具

javascript 复制代码
// ✅ 统一的查询模式
function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
}

function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId, // 条件式获取
  });
}

// ✅ 在组件中使用
function MyComponent() {
  const { data: users } = useUsers();
  const { data: user } = useUser(1);
  
  // 简洁明了,没有样板代码
}

// ✅ React Query Devtools - 可视化调试
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
      <ReactQueryDevtools initialIsOpen={false} />
      {/* 在开发环境中可以看到所有查询的状态、缓存、时间线 */}
    </QueryClientProvider>
  );
}
相关推荐
Icoolkj8 小时前
npm、npx、pnpm 深度解析:从原理到实战的全方位指南
前端·npm·node.js
尘埃不入你眼眸8 小时前
powerShell无法执行npm问题
前端·npm·node.js
我是一只懒羊羊8 小时前
从零搭建 Node.js企业级 Web 服务器:自定义服务&数据请求
前端·node.js·全栈
itslife8 小时前
vite 源码 -
前端·javascript
我有一棵树8 小时前
npm uninstall 执行的操作、有时不会删除 node_modules 下对应的文件夹
前端·npm·node.js
叫我詹躲躲8 小时前
Linux 服务器磁盘满了?教你快速找到大文件,安全删掉不踩坑!
linux·前端·curl
Mintopia8 小时前
动态数据驱动的 AIGC 模型:Web 端实时更新训练的技术可行性
前端·javascript·aigc
志摩凛8 小时前
前端必备技能:使用 appearance: none 实现完美自定义表单控件
前端·css
枕梦1268 小时前
Elpis:企业级配置化框架的设计与实践
前端