TanStack Query
安装
pnpm add @tanstack/react-query
配置 QueryClient
在应用的根组件中配置 QueryClient:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// 创建 QueryClient 实例
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 全局默认配置
staleTime: 5 * 60 * 1000, // 5分钟
cacheTime: 10 * 60 * 1000, // 10分钟
retry: 3, // 失败重试次数
refetchOnWindowFocus: false, // 窗口聚焦时不重新获取
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* 你的应用组件 */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
基本使用
1️⃣ 简单的数据获取
import { useQuery } from "@tanstack/react-query";
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ["todos"],
queryFn: async () => {
const response = await fetch("/api/todos");
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
},
});
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<ul>
{data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
2️⃣ 带参数的查询
function TodoDetail({ todoId }) {
const { data, isLoading, error } = useQuery({
queryKey: ["todo", todoId],
queryFn: async () => {
const response = await fetch(`/api/todos/${todoId}`);
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
},
enabled: !!todoId, // 只有当 todoId 存在时才执行查询
});
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<div>
<h1>{data?.title}</h1>
<p>{data?.description}</p>
</div>
);
}
查询状态 🔍
useQuery 返回的对象包含以下状态:
const {
data, // 查询成功的数据
isLoading, // 是否正在加载(首次加载)
isFetching, // 是否正在获取数据(包括后台刷新)
isError, // 是否有错误
error, // 错误对象
isSuccess, // 是否成功
status, // 状态字符串:'idle' | 'loading' | 'error' | 'success'
fetchStatus, // 获取状态:'idle' | 'fetching' | 'paused'
} = useQuery({ ... })
查询配置选项
1️⃣ 缓存和重新获取
const { data } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // 数据在 5 分钟内被认为是新鲜的
cacheTime: 10 * 60 * 1000, // 数据在缓存中保留 10 分钟
refetchOnWindowFocus: false, // 窗口聚焦时不重新获取
refetchOnMount: true, // 组件挂载时重新获取
refetchOnReconnect: true, // 网络重连时重新获取
});
2️⃣ 错误处理
const { data, error, isError } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
retry: 3, // 失败重试 3 次
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
onError: (error) => {
console.error("查询失败:", error);
},
onSuccess: (data) => {
console.log("查询成功:", data);
},
});
3️⃣ 条件查询
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // 只有当 userId 存在时才执行
enabled: user.isAuthenticated, // 或者基于其他条件
});
手动控制查询
const { data, refetch, remove } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
// 手动重新获取数据
const handleRefresh = () => {
refetch();
};
// 清除缓存
const handleClearCache = () => {
remove();
};
查询键(Query Keys)
查询键用于标识和缓存查询:
// 简单键
useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
// 带参数的键
useQuery({ queryKey: ["todo", todoId], queryFn: () => fetchTodo(todoId) });
// 复杂键
useQuery({
queryKey: ["todos", { status: "done", userId: 123 }],
queryFn: () => fetchTodos({ status: "done", userId: 123 }),
});
// 数组键
useQuery({
queryKey: ["todos", filters, sortBy],
queryFn: () => fetchTodos(filters, sortBy),
});
预取数据
import { useQueryClient } from "@tanstack/react-query";
function TodoList() {
// 获取全局的 QueryClient 实例
const queryClient = useQueryClient();
const prefetchTodo = async (todoId) => {
// prefetchQuery 是 React Query 提供的预取数据的方法
// 提前将数据请求到缓存 (Cache) 中,但不会触发组件重新渲染
await queryClient.prefetchQuery({
queryKey: ["todo", todoId],
queryFn: () => fetchTodo(todoId),
});
};
return (
<div>
<button onClick={() => prefetchTodo(1)}>预取 Todo 1</button>
</div>
);
}
无限查询
对于分页数据,可以使用 useInfiniteQuery
:
import { useInfiniteQuery } from "@tanstack/react-query";
function InfiniteTodoList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
queryKey: ["infiniteTodos"],
queryFn: ({ pageParam = 0 }) => fetchTodos({ page: pageParam }),
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length : undefined;
},
});
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? "加载更多..." : hasNextPage ? "加载更多" : "没有更多数据"}
</button>
</div>
);
}
useMutation - 数据修改操作
useMutation
用于处理数据的修改操作,如创建、更新、删除等。
1️⃣ 基本用法
import { useMutation, useQueryClient } from "@tanstack/react-query";
function CreateTodo() {
// 获取全局的 QueryClient 实例
const queryClient = useQueryClient();
// 创建一个用于创建待办事项的 Mutation
const createTodoMutation = useMutation({
mutationFn: (newTodo) => {
return fetch("/api/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newTodo),
}).then((res) => res.json());
},
onSuccess: (data) => {
console.log("创建成功:", data);
// 可以在这里显示成功消息
// 也可以刷新缓存
// queryClient.invalidateQueries({ queryKey: ["todos"] });
},
onError: (error) => {
console.error("创建失败:", error);
// 可以在这里显示错误消息
},
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const newTodo = {
title: formData.get("title"),
completed: false,
};
// 调用 Mutation 的 mutate 方法来触发数据修改操作
createTodoMutation.mutate(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="输入待办事项" required />
<button type="submit" disabled={createTodoMutation.isPending}>
{createTodoMutation.isPending ? "创建中..." : "创建"}
</button>
</form>
);
}
2️⃣ Mutation 状态
const mutation = useMutation({
mutationFn: updateTodo,
});
// 可用的状态
console.log(mutation.isIdle); // 空闲状态
console.log(mutation.isPending); // 正在执行
console.log(mutation.isSuccess); // 执行成功
console.log(mutation.isError); // 执行失败
console.log(mutation.data); // 成功返回的数据
console.log(mutation.error); // 错误信息
3️⃣ 手动触发和参数传递
function TodoItem({ todo }) {
const updateTodoMutation = useMutation({
mutationFn: ({ id, updates }) => {
return fetch(`/api/todos/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
}).then((res) => res.json());
},
});
const handleToggle = () => {
updateTodoMutation.mutate({
id: todo.id,
updates: { completed: !todo.completed },
});
};
const handleDelete = () => {
updateTodoMutation.mutate({
id: todo.id,
updates: { deleted: true },
});
};
return (
<div>
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>{todo.title}</span>
<button onClick={handleToggle} disabled={updateTodoMutation.isPending}>
切换状态
</button>
<button onClick={handleDelete} disabled={updateTodoMutation.isPending}>
删除
</button>
</div>
);
}
4️⃣ 乐观更新
结合 useQueryClient
实现乐观更新:
import { useMutation, useQueryClient } from "@tanstack/react-query";
function TodoItem({ todo }) {
const queryClient = useQueryClient();
const updateTodoMutation = useMutation({
mutationFn: (updatedTodo) => updateTodoApi(updatedTodo),
onMutate: async (newTodo) => {
// 取消正在进行的查询
await queryClient.cancelQueries({ queryKey: ["todos"] });
// 保存之前的数据
const previousTodos = queryClient.getQueryData(["todos"]);
// 乐观更新 - 立即更新 UI
queryClient.setQueryData(["todos"], (old) =>
old.map((todo) => (todo.id === newTodo.id ? { ...todo, ...newTodo } : todo))
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 出错时回滚到之前的状态
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
// 完成后重新获取数据确保同步
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<div>
<span>{todo.title}</span>
<button
onClick={() =>
updateTodoMutation.mutate({
id: todo.id,
completed: !todo.completed,
})
}
disabled={updateTodoMutation.isPending}
>
切换完成状态
</button>
</div>
);
}
useQueryClient - 查询客户端
useQueryClient
提供了对查询客户端的访问,用于手动管理缓存、预取数据等操作。
基本用法
import { useQueryClient } from "@tanstack/react-query";
function TodoManager() {
const queryClient = useQueryClient();
// 预取数据
const prefetchTodo = async (todoId) => {
await queryClient.prefetchQuery({
queryKey: ["todo", todoId],
queryFn: () => fetchTodo(todoId),
});
};
// 手动设置缓存数据
const setCachedData = () => {
queryClient.setQueryData(["todos"], [{ id: 1, title: "预加载的待办事项", completed: false }]);
};
// 获取缓存数据
const getCachedData = () => {
const todos = queryClient.getQueryData(["todos"]);
console.log("缓存的待办事项:", todos);
};
// 清除特定查询的缓存
const clearCache = () => {
queryClient.removeQueries({ queryKey: ["todos"] });
};
// 使查询失效(触发重新获取)
const invalidateCache = () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
};
return (
<div>
<button onClick={() => prefetchTodo(1)}>预取 Todo 1</button>
<button onClick={setCachedData}>设置缓存数据</button>
<button onClick={getCachedData}>获取缓存数据</button>
<button onClick={clearCache}>清除缓存</button>
<button onClick={invalidateCache}>使缓存失效</button>
</div>
);
}
缓存管理
function CacheManager() {
const queryClient = useQueryClient();
// 获取所有查询
const getAllQueries = () => {
const queries = queryClient.getQueriesData();
console.log("所有查询:", queries);
};
// 获取特定查询
const getSpecificQuery = () => {
const todoQuery = queryClient.getQueryData(["todos"]);
const userQuery = queryClient.getQueryData(["user", 123]);
console.log("待办事项查询:", todoQuery);
console.log("用户查询:", userQuery);
};
// 设置查询数据
const setQueryData = () => {
queryClient.setQueryData(["todos"], (oldData) => {
// 可以基于旧数据更新
return oldData ? [...oldData, { id: 999, title: "新项目", completed: false }] : [];
});
};
// 重置查询状态
const resetQuery = () => {
queryClient.resetQueries({ queryKey: ["todos"] });
};
return (
<div>
<button onClick={getAllQueries}>获取所有查询</button>
<button onClick={getSpecificQuery}>获取特定查询</button>
<button onClick={setQueryData}>设置查询数据</button>
<button onClick={resetQuery}>重置查询</button>
</div>
);
}
查询状态管理
function QueryStateManager() {
const queryClient = useQueryClient();
// 检查查询是否正在获取
const checkFetching = () => {
const isFetching = queryClient.isFetching();
const todosFetching = queryClient.isFetching({ queryKey: ["todos"] });
console.log("是否有查询正在获取:", isFetching);
console.log("待办事项是否正在获取:", todosFetching);
};
// 获取查询状态
const getQueryState = () => {
const queryState = queryClient.getQueryState(["todos"]);
console.log("查询状态:", queryState);
};
// 设置查询状态
const setQueryState = () => {
queryClient.setQueryData(["todos"], (old) => old || []);
};
return (
<div>
<button onClick={checkFetching}>检查获取状态</button>
<button onClick={getQueryState}>获取查询状态</button>
<button onClick={setQueryState}>设置查询状态</button>
</div>
);
}
实际应用场景
1. 表单提交后更新列表
function TodoForm() {
const queryClient = useQueryClient();
const createTodoMutation = useMutation({
mutationFn: createTodoApi,
onSuccess: (newTodo) => {
// 提交成功后,更新缓存中的待办事项列表
queryClient.setQueryData(["todos"], (oldTodos) => {
return oldTodos ? [...oldTodos, newTodo] : [newTodo];
});
// 或者使缓存失效,重新获取最新数据
// queryClient.invalidateQueries({ queryKey: ['todos'] })
},
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
createTodoMutation.mutate({
title: formData.get("title"),
completed: false,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="新待办事项" required />
<button type="submit" disabled={createTodoMutation.isPending}>
{createTodoMutation.isPending ? "创建中..." : "创建"}
</button>
</form>
);
}
2. 批量操作
function BatchOperations() {
const queryClient = useQueryClient();
const batchUpdateMutation = useMutation({
mutationFn: batchUpdateApi,
onMutate: async (todoIds) => {
// 乐观更新:立即标记为完成
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (oldTodos) =>
oldTodos.map((todo) => (todoIds.includes(todo.id) ? { ...todo, completed: true } : todo))
);
return { previousTodos };
},
onError: (err, todoIds, context) => {
// 出错时回滚
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
// 完成后重新获取确保同步
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const handleBatchComplete = (todoIds) => {
batchUpdateMutation.mutate(todoIds);
};
return (
<button onClick={() => handleBatchComplete([1, 2, 3])} disabled={batchUpdateMutation.isPending}>
{batchUpdateMutation.isPending ? "批量更新中..." : "批量标记完成"}
</button>
);
}
最佳实践
1. 自定义 Hook
// hooks/useTodos.ts
export function useTodos() {
return useQuery({
queryKey: ["todos"],
queryFn: async () => {
const response = await fetch("/api/todos");
if (!response.ok) {
throw new Error("Failed to fetch todos");
}
return response.json();
},
});
}
// hooks/useTodoMutations.ts
export function useCreateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createTodoApi,
onSuccess: (newTodo) => {
queryClient.setQueryData(["todos"], (oldTodos) => (oldTodos ? [...oldTodos, newTodo] : [newTodo]));
},
});
}
// 使用
function TodoList() {
const { data, isLoading, error } = useTodos();
const createTodo = useCreateTodo();
// ...
}
2. 错误边界
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
useErrorBoundary: true, // 将错误传播到最近的错误边界
});
if (isLoading) return <div>加载中...</div>;
return <div>{/* 渲染数据 */}</div>;
}
3. 乐观更新
结合 useMutation
实现乐观更新:
import { useMutation, useQueryClient } from "@tanstack/react-query";
function TodoItem({ todo }) {
const queryClient = useQueryClient();
const updateTodo = useMutation({
mutationFn: (updatedTodo) => updateTodoApi(updatedTodo),
onMutate: async (newTodo) => {
// 取消正在进行的查询
await queryClient.cancelQueries({ queryKey: ["todos"] });
// 保存之前的数据
const previousTodos = queryClient.getQueryData(["todos"]);
// 乐观更新
queryClient.setQueryData(["todos"], (old) => old.map((todo) => (todo.id === newTodo.id ? newTodo : todo)));
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 出错时回滚
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
// 完成后重新获取数据
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<div>
<span>{todo.title}</span>
<button onClick={() => updateTodo.mutate({ ...todo, completed: !todo.completed })}>切换完成状态</button>
</div>
);
}
总结
@tanstack/react-query 提供了强大的数据获取和缓存功能:
- 自动缓存:减少不必要的网络请求
- 后台更新:保持数据新鲜度
- 错误处理:内置重试和错误状态管理
- 加载状态:提供多种加载状态
- 乐观更新:提升用户体验
- 无限查询:处理分页数据
- 预取:提前加载可能需要的数据
通过合理配置和使用这些功能,可以大大简化 React 应用中的数据管理逻辑。