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 执行: 刷新列表
四.如何使用?
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>
)
当用户退出或者切换公司的时候需要去清除缓存。
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>;
}