阅读视图

发现新文章,点击刷新页面。

tanstack query的基本使用

一.为什么要使用tanstack query?

显式的来看:主要是可以简化代码,可以看下面的一个例子,这是一个我们经常用的发送请求的一个过程,包括了请求数据,加载处理,还有错误展示。
隐式的来看:除了简化代码,还有自动缓存,可配置的数据过期时间,请求去重,乐观更新,并行和依赖查询等等好处

import { Button } from "antd";
import { useState, useEffect } from "react";

function Home() {
  const [data, setData] = useState<{ id: number, name: string }[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  useEffect(() => {
    setIsLoading(true);
    fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json()).then(data => {
      setData(data);
    }).catch(err => {
      setError(err as Error);
    }).finally(()=>{
      setIsLoading(false);
    })
  },[])
  return (
    <>
      <div>Home页面</div>
      <Button type="primary">Button</Button>
      <div className="mt-10 ml-10">
        {isLoading ? <div>Loading...</div> : null}
        {
          data?.map((item) => {
            return <div key={item.id}>{item.name}</div>
          })
        }
        {error ? <div>Error: {error.message}</div> : null}
      </div>
    </>
  );
}
export default Home;

使用tanstack query之后,代码量减少了很多很多

import { Button } from "antd";
import { useQuery } from "@tanstack/react-query";

function Home() {
  const {data,isLoading,error} = useQuery<{ id: number, name: string }[]>({
    queryKey: ['users'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json())
  })

  return (
    <>
      <div>Home页面</div>
      <Button type="primary">Button</Button>
      <div className="mt-10 ml-10">
        {isLoading ? <div>Loading...</div> : null}
        {
          data?.map((item) => {
            return <div key={item.id}>{item.name}</div>
          })
        }
        {error ? <div>Error: {error.message}</div> : null}
      </div>
    </>
  );
}
export default Home;

二.学习网址

官方地址:tanstack.com/query/lates…

三.tanstack query的好处:

a.自动缓存

在第一次调用的时候,都会去后台拿数据。但是在再次调用的时候,tanstack因为有缓存,所以会先从缓存中把之前的数据拿出来渲染,同时去请求新的数据,然后把新的数据替换掉data。

// 不使用 TanStack Query
const [data, setData] = useState(null);
useEffect(() => {
  fetchData().then(setData);
}, []);

// 使用 TanStack Query
const { data } = useQuery(['key'], fetchData);
// 自动缓存!后续调用瞬间返回

b.可配置的数据过期时间和重新获取间隔

a.重新获取间隔staleTime

默认情况下,通过 useQuery 或 useInfiniteQuery 查询实例将缓存的数据视为过时的数据。
但是在配置了staleTime之后,比如将 staleTime 设置为例如 30 * 1000,会确保从缓存中读取数据,而不触发任何类型的重新获取,持续 30秒,或直到 Query 手动失效。意思就是30s内再去请求这个接口,不会从后台拿数据,而是从缓存中拿数据。

import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
  const { data, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    // 数据在30秒内被视为"新鲜",不会重新获取
    staleTime: 1000 * 30, // 30秒
    // 数据在5分钟后会被视为"过期",下次访问时会后台重新获取
    gcTime: 1000 * 60 * 5, // 5分钟 (v5版本以前叫 cacheTime)
    // 窗口重新聚焦时,如果数据已过期,重新获取
    refetchOnWindowFocus: true,
    // 组件重新挂载时,如果数据已过期,重新获取
    refetchOnMount: true,
    // 网络重新连接时重新获取
    refetchOnReconnect: true,
  });
}

b.数据过期时间gcTime

这个指的是数据存在缓存中的时间。有一点要注意:只有组件卸载后<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">gcTime</font> 才会开始倒计时,倒计时结束后缓存被清理。下次组件挂载时如果没有缓存,才会重新请求。
比如以下代码:

const {data,isLoading,error} = useQuery<{ id: number, name: string }[]>({
    queryKey: ['users'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json()),
    staleTime: 1000 * 30,
    gcTime: 1000,
  })

staleTime为30s,gcTime为1s。这个是在home页面上的, 如果离开home页面1s以上,再进入到home页面,则会从新请求。 如果在离开home页面1s以内重新进入,那么还是会从缓存中取数据(因为1s以内缓存还没被清理)。

c.并行和依赖查询

a.并行查询

不需要其他的操作,直接写发起请求的就会同时去请求(主要是告诉你useQuery有这个特性(这样子写就会并行查询))

function App () {
  // The following queries will execute in parallel
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
  const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
  ...
}

也可以使用useQueries来批量查询

queries接受的是一个查询的数组,返回的userQueries是一个查询结果的数组

function App({ users }) {
  const userQueries = useQueries({
    queries: users.map((user) => {
      return {
        queryKey: ['user', user.id],
        queryFn: () => fetchUserById(user.id),
      }
    }),
  })
}

b.依赖查询eabled

这里主要用到了enabled这个属性,这个属性接受的是一个boolean的变量。以下的意思为先去请求users的数据,然后data有数据之后再去请求articleList的数据。这个还是挺重要的,比如跳转到详情页,需要先拿到id,有id才能去查询数据

const {data,isLoading,error} = useQuery<{ id: number, name: string }[]>({
    queryKey: ['users'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json()),
    staleTime: 1000 * 30,
    gcTime: 1000,
  })
  console.log('data',data);

  const { data: articleList } = useQuery<{ id: number, title: string }[]>({
    queryKey: ['articleList'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json()),
    enabled: !!data
  })
  console.log('articleList',articleList);

d.完整的加载状态管理

这里有很多状态,但是一般就是用到data和isLoading最多,但是这里列出来让大家都知道

function DataComponent() {
  const query = useQuery({
    queryKey: ['data'],
    queryFn: fetchData,
  });

  // 丰富的状态信息
  const {
    data,           // 成功后的数据
    isLoading,      // 首次加载中
    isFetching,     // 任何请求进行中(包括后台)
    isPending,      // 没有数据且没有加载
    isError,        // 是否错误
    isSuccess,      // 是否成功
    error,          // 错误对象
    status,         // 'pending' | 'error' | 'success'
    fetchStatus,    // 'fetching' | 'paused' | 'idle'
    dataUpdatedAt,  // 最后更新时间
    errorUpdatedAt, // 最后错误时间
    failureCount,   // 失败次数
    failureReason,  // 失败原因
    errorUpdateCount, // 错误更新次数
    isPaused,       // 是否因网络暂停
    isRefetching,   // 是否正在重新获取
  } = query;

  if (isLoading) return <Skeleton />;
  if (isError) return <Error message={error.message} />;
  
  return (
    <div>
      <DataView data={data} />
      {isFetching && <BackgroundRefreshIndicator />}
    </div>
  );
}

e.请求去重

就是多个页面同时都用到了这个请求的话,会主动去重,不会同一时间多次请求

// API 函数
const fetchProducts = async (category) => {
  console.log('📦 发起商品请求', new Date().toLocaleTimeString());
  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟慢请求
  const res = await fetch(`/api/products/${category}`);
  return res.json();
};

// 商品列表组件
function ProductList({ category }) {
  const { data: products } = useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
  });

  return <div>{products?.map(p => <ProductCard key={p.id} product={p} />)}</div>;
}

// 商品数量统计组件
function ProductCount({ category }) {
  const { data: products } = useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
  });
  return <span>总数: {products?.length}</span>;
}

// 商品分类导航组件
function CategoryNav({ category }) {
  const { data: products } = useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
  });

  return <div>分类: {category} ({products?.length}件商品)</div>;
}

// 页面:三个组件同时渲染
function CategoryPage({ category }) {
  return (
    <div>
      <CategoryNav category={category} />
      <ProductCount category={category} />
      <ProductList category={category} />
      {/* 🔥 尽管三个组件都请求相同数据 */}
      {/* ✅ 但只会发起一次API请求 */}
      {/* ✅ 节省带宽,提高性能 */}
    </div>
  );
}

f.乐观更新

什么是乐观更新?

乐观更新就是:假设操作会成功,先更新UI,再发请求。

简单说就是:先给用户看结果,再去服务器确认。

传统方式:
点击按钮 ──> 显示加载 ──> 等待500ms ──> 服务器响应 ──> 更新UI
             用户感觉:慢,有延迟

乐观更新:
点击按钮 ──> 立即更新UI ──> 后台发送请求 ──> 服务器响应
             用户感觉:瞬间响应,像本地操作
const queryClient = useQueryClient()

useMutation({
  // 1. mutationFn: 实际发送请求的函数
  mutationFn: updateTodo,  // 调用API更新待办
  
  // 2. onMutate: 乐观更新的核心!
  onMutate: async (newTodo, context) => {
    // ⚠️ 注意:这里的参数是 (newTodo, context)
    // newTodo: 要更新的数据
    // context: 包含 client (QueryClient 实例)
    
    // 2.1 取消任何正在进行的查询
    // 为什么要取消?避免旧数据覆盖我们的乐观更新
    await context.client.cancelQueries({ queryKey: ['todos'] })
    
    // 2.2 保存当前数据的快照
    // 为什么要保存?如果失败需要回滚到这个状态
    const previousTodos = context.client.getQueryData(['todos'])
    
    // 2.3 乐观更新:立即更新UI
    // 不等服务器响应,先把新待办加到列表里
    context.client.setQueryData(['todos'], (old) => [...old, newTodo])
    
    // 2.4 返回快照,供onError使用
    return { previousTodos }
  },
  
  // 3. onError: 如果请求失败
  onError: (err, newTodo, onMutateResult, context) => {
    // ⚠️ 参数:(错误, 变量, onMutate返回的结果, context)
    
    // 用之前保存的快照恢复数据
    context.client.setQueryData(['todos'], onMutateResult.previousTodos)
    // 效果:新添加的待办消失,回到之前的状态
  },
  
  // 4. onSettled: 无论成功失败都会执行
  onSettled: (data, error, variables, onMutateResult, context) => {
    // 重新获取最新的待办列表,确保与服务器同步
    context.client.invalidateQueries({ queryKey: ['todos'] })
    // 这样即使乐观更新成功了,也会再次确认服务器数据
  },
})
// 假设初始待办列表
const initialTodos = [
  { id: 1, title: '学习React', completed: false },
  { id: 2, title: '写代码', completed: false },
]

// 用户添加新待办
addTodoMutation.mutate({ title: '新任务', completed: false })

// 时间线:
时间 0ms: 用户点击"添加"按钮
        ↓
时间 0ms: onMutate 执行
        • 取消进行中的查询
        • 保存快照: [{id:1}, {id:2}]
        • 立即更新UI: [{id:1}, {id:2}, {title:'新任务'}]
        • 用户立即看到新任务 ✓
        ↓
时间 0ms: mutationFn 开始发送请求到服务器
        ↓
时间 100ms: 请求成功 ✅
        onSettled 执行: 刷新列表
        ↓
时间 100ms: 列表刷新,确认数据同步

// 如果请求失败 ❌
时间 100ms: 请求失败
        ↓
时间 100ms: onError 执行
        • 用快照恢复: [{id:1}, {id:2}]
        • 新任务消失
        • 用户看到错误提示
        ↓
时间 100ms: onSettled 执行: 刷新列表

四.如何使用?

参考: tanstack.com/query/lates…

1.安装

npm i @tanstack/react-query

2.导入

import { createRoot } from 'react-dom/client'
import '@/assets/css/index.css'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from '@/store'

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <Provider store={store}>
      <RouterProvider router={router}></RouterProvider>
    </Provider>
  </QueryClientProvider>
)

3.使用

a.使用useQuery查询数据

import { Button } from "antd";
import { useQuery } from "@tanstack/react-query";

function Home() {
  const {data,isLoading,error} = useQuery<{ id: number, name: string }[]>({
    queryKey: ['users'],
    queryFn: () => fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json())
  })

  return (
    <>
      <div>Home页面</div>
      <Button type="primary">Button</Button>
      <div className="mt-10 ml-10">
        {isLoading ? <div>Loading...</div> : null}
        {
          data?.map((item) => {
            return <div key={item.id}>{item.name}</div>
          })
        }
        {error ? <div>Error: {error.message}</div> : null}
      </div>
    </>
  );
}
export default Home;

b.使用useMutation来更新数据

其实就是在useMutation定义好mutation的function,成功的回调,失败的回调。 然后真的用户做保存动作时,去调用返回的实例中的mutate方法传入参数,然后就会去调用mutationFn的方法,调用成功后走onSuccess的逻辑。

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

interface Todo {
  title: string;
  completed: boolean;
  userId: number;
}

export default function AddTodo() {
  const [title, setTitle] = useState('');
  const queryClient = useQueryClient();

  // 基础 mutation(解构出常用状态)
  const { mutate, isPending, isError, isSuccess, error } = useMutation({
    // 1. mutationFn: 执行实际操作的函数
    mutationFn: (newTodo: Todo) => 
      fetch('https://jsonplaceholder.typicode.com/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
        headers: {
          'Content-type': 'application/json; charset=UTF-8',
        },
      }).then(res => res.json()),

    // 2. onSuccess: 成功后的回调
    onSuccess: (data) => {
      console.log('添加成功:', data);
      // 刷新待办列表
      queryClient.invalidateQueries({ queryKey: ['todos'] });
      // 清空输入框
      setTitle('');
    },

    // 3. onError: 失败后的回调
    onError: (error) => {
      console.error('添加失败:', error);
    },

    // 4. onSettled: 无论成功失败都会执行
    onSettled: (data, error) => {
      console.log('操作完成', { data, error });
    },
  });

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!title.trim()) return;
    
    // 执行 mutation
    mutate({
      title: title,
      completed: false,
      userId: 1,
    });
  };

  return (
    <div className="add-todo">
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="输入待办事项"
          disabled={isPending}
        />
        <button 
          type="submit"
          disabled={isPending}
        >
          {isPending ? '添加中...' : '添加待办'}
        </button>
      </form>
      
      {/* 显示状态 */}
      {isPending && (
        <div className="loading-indicator">⏳ 正在添加...</div>
      )}
      
      {isError && (
        <div className="error-message">
          ❌ {error?.message}
        </div>
      )}
      
      {isSuccess && (
        <div className="success-message">✅ 添加成功!</div>
      )}
    </div>
  );
}

4.使用全局自定义配置(可选)

在main.ts中可以全局自定义配置(所有的请求在自己没配置的时候会使用全局配置。)

import { createRoot } from 'react-dom/client'
import '@/assets/css/index.css'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from '@/store'
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 失败后重试次数(默认 3 次),设为 1 或 false 可减少不必要的请求
      retry: 1,
      // 重试延迟(默认指数退避),可自定义
      // retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),

      // 数据过期时间(默认 0,即立即过期)。5 分钟内相同 queryKey 的请求会直接用缓存
      staleTime: 1000 * 60 * 5,
      // 非活跃数据在缓存中保留的时间(默认 5 分钟),超时后垃圾回收
      gcTime: 1000 * 60 * 10,

      // 窗口重新聚焦时是否自动重新请求(默认 true)
      refetchOnWindowFocus: false,
      // 网络重新连接时是否自动重新请求(默认 true)
      refetchOnReconnect: true,
      // 组件重新挂载时是否自动重新请求(默认 true)
      refetchOnMount: true,
    },
    mutations: {
      // mutation 失败后重试次数(默认 0,即不重试)
      retry: 0,
      // 全局 mutation 错误处理
      // onError: (error) => {
      //   console.error('全局 mutation 错误:', error);
      // },
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <Provider store={store}>
      <RouterProvider router={router}></RouterProvider>
    </Provider>
  </QueryClientProvider>
)

五.常用的hook

1.useQuery - 数据获取之王

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

function UserProfile({ userId }) {
  // 最基础的数据查询
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
  });

  if (isLoading) return <Loading />;
  if (error) return <Error />;
  return <div>{data.name}</div>;
}

// 常用属性
const {
  data,           // 返回的数据
  isLoading,      // 首次加载中
  isFetching,     // 任何请求进行中
  error,          // 错误对象
  isError,        // 是否错误
  isSuccess,      // 是否成功
  status,         // 'pending' | 'error' | 'success'
  refetch,        // 手动重新获取
  remove,         // 移除缓存
} = useQuery(...);

2. useMutation - 数据修改之王

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

function AddTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newTodo) => 
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(res => res.json()),
    
    onSuccess: () => {
      // 成功后刷新列表
      queryClient.invalidateQueries(['todos']);
      toast.success('添加成功!');
    },
    
    onError: (error) => {
      toast.error(`失败:${error.message}`);
    },
  });

  return (
    <button 
      onClick={() => mutation.mutate({ title: '新任务' })}
      disabled={mutation.isLoading}
    >
      {mutation.isLoading ? '添加中...' : '添加待办'}
    </button>
  );
}

// 常用属性
const {
  mutate,         // 执行 mutation
  mutateAsync,    // 异步执行
  isLoading,      // 是否加载中
  isError,        // 是否错误
  isSuccess,      // 是否成功
  data,           // 返回数据
  error,          // 错误对象
  reset,          // 重置状态
} = useMutation(...);

3.QueryClient

new这个QueryClient会生成一个queryClient的实例,这个实例就包含了各种的方法。

可以配置全局自定义配置,可以清除缓存。这里一般在main.tsx中生成这个实例,然后使用QueryClientProvider传递给子孙组件,后代组件可以使用useQueryClient拿到这个实例对象,从而操作它的方法。

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 失败后重试次数(默认 3 次),设为 1 或 false 可减少不必要的请求
      retry: 1,
      // 重试延迟(默认指数退避),可自定义
      // retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
      // 数据过期时间(默认 0,即立即过期)。5 分钟内相同 queryKey 的请求会直接用缓存
      staleTime: 1000 * 60 * 5,
      // 非活跃数据在缓存中保留的时间(默认 5 分钟),超时后垃圾回收
      gcTime: 1000 * 60 * 10,
      // 窗口重新聚焦时是否自动重新请求(默认 true)
      refetchOnWindowFocus: false,
      // 网络重新连接时是否自动重新请求(默认 true)
      refetchOnReconnect: true,
      // 组件重新挂载时是否自动重新请求(默认 true)
      refetchOnMount: true,
    },
    mutations: {
      // mutation 失败后重试次数(默认 0,即不重试)
      retry: 0,
      // 全局 mutation 错误处理
      // onError: (error) => {
      //   console.error('全局 mutation 错误:', error);
      // },
    },
  },
})
createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <Provider store={store}>
      <RouterProvider router={router}></RouterProvider>
    </Provider>
  </QueryClientProvider>
)

链接:tanstack.com/query/lates…

当用户退出或者切换公司的时候需要去清除缓存。

queryClient.clear()

4.useQueryClient - 缓存操作神器

使用useQueryClient拿到queryClient实例。

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

function TodoManager() {
  const queryClient = useQueryClient();

  // 常用方法
  const handleRefresh = () => {
    // 1. 刷新特定查询
    queryClient.invalidateQueries(['todos']);
    
    // 2. 直接设置缓存
    queryClient.setQueryData(['todos'], newTodos);
    
    // 3. 获取缓存数据
    const todos = queryClient.getQueryData(['todos']);
    
    // 4. 取消进行中的查询
    queryClient.cancelQueries(['todos']);
    
    // 5. 预加载数据
    queryClient.prefetchQuery({
      queryKey: ['todos', 'next'],
      queryFn: fetchNextTodos,
    });
    
    // 6. 重置查询
    queryClient.resetQueries(['todos']);
    
    // 7. 移除缓存
    queryClient.removeQueries(['todos']);
    
    // 8. 清空所有缓存
    queryClient.clear();
  };

  return <button onClick={handleRefresh}>刷新</button>;
}

六.demo地址

地址:gitee.com/rui-rui-an/…

❌