TanStack Query
TanStack Query(原 React Query)是一个强大的异步状态管理器,支持多个前端框架,包括 React、Vue、Solid 等。
核心概念
- Queries: 用于数据获取
- Mutations: 用于数据修改
- Query Invalidation: 查询失效处理
- Caching: 缓存机制
- Background Updates: 后台更新
- Optimistic Updates: 乐观更新
React 使用
1. 安装和配置
npm install @tanstack/react-query
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
cacheTime: 1000 * 60 * 30, // 30分钟
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
2. 基本查询
import { useQuery } from '@tanstack/react-query';
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
staleTime: 5000,
cacheTime: 300000,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
3. 带参数的查询
function Todo({ todoId }) {
const { data, isLoading } = useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodoById(todoId),
enabled: !!todoId,
});
if (isLoading) return <div>Loading...</div>;
return <div>{data.title}</div>;
}
4. 修改数据
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
},
onSuccess: () => {
// 使相关查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
mutation.mutate({ title: 'New Todo' });
}}>
<button type="submit">Add Todo</button>
</form>
);
}
5. 乐观更新
function TodoList() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消任何传出的重新获取
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 获取之前的值
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], (old) => {
return old.map(todo =>
todo.id === newTodo.id ? newTodo : todo
);
});
// 返回上下文
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 发生错误时回滚
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
}
Vue 使用
1. 安装和配置
npm install @tanstack/vue-query
<!-- App.vue -->
<script setup>
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query';
import { createApp } from 'vue';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
},
},
});
const app = createApp(App);
app.use(VueQueryPlugin, {
queryClient,
});
</script>
2. 组合式 API 查询
<script setup>
import { useQuery } from '@tanstack/vue-query';
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
});
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="todo in data" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</template>
3. 带参数的查询
<script setup>
import { useQuery } from '@tanstack/vue-query';
import { ref } from 'vue';
const todoId = ref(1);
const { data, isLoading } = useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodoById(todoId.value),
enabled: computed(() => !!todoId.value),
});
</script>
4. 修改数据
<script setup>
import { useMutation, useQueryClient } from '@tanstack/vue-query';
const queryClient = useQueryClient();
const { mutate, isLoading } = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
</script>
<template>
<button
@click="mutate({ title: 'New Todo' })"
:disabled="isLoading"
>
{{ isLoading ? 'Adding...' : 'Add Todo' }}
</button>
</template>
5. 乐观更新
<script setup>
import { useMutation, useQueryClient } from '@tanstack/vue-query';
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => {
return old.map(todo =>
todo.id === newTodo.id ? newTodo : todo
);
});
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
</script>
进阶用法
1. 依赖查询
当一个查询依赖于另一个查询的结果时:
// React 示例
function UserPosts() {
// 首先获取用户
const { data: user } = useQuery({
queryKey: ['user', 'current'],
queryFn: getCurrentUser,
});
// 基于用户ID获取帖子
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user.id),
// 只在有用户ID时才执行查询
enabled: !!user?.id,
});
return (
<div>
<h1>{user?.name}'s Posts</h1>
{posts?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
// Vue 示例
const { data: user } = useQuery({
queryKey: ['user', 'current'],
queryFn: getCurrentUser,
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.value?.id],
queryFn: () => fetchUserPosts(user.value.id),
enabled: computed(() => !!user.value?.id),
});
2. 动态并行查询
当需要同时执行多个动态数量的查询时:
// React 示例
function UserProfiles({ userIds }) {
const userQueries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUserById(id),
staleTime: 1000 * 60 * 5,
})),
});
return (
<div>
{userQueries.map(({ data, isLoading }, index) => (
<div key={userIds[index]}>
{isLoading ? (
<Spinner />
) : (
<UserProfile user={data} />
)}
</div>
))}
</div>
);
}
// Vue 示例
const userIds = ref([1, 2, 3]);
const queries = computed(() =>
userIds.value.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUserById(id),
}))
);
const results = useQueries({
queries,
});
3. 复杂的乐观更新
处理复杂的乐观更新场景,包括关联数据更新:
function ComplexTodoList() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消相关查询
await Promise.all([
queryClient.cancelQueries({ queryKey: ['todos'] }),
queryClient.cancelQueries({ queryKey: ['todo', newTodo.id] }),
queryClient.cancelQueries({ queryKey: ['todoStats'] }),
]);
// 快照之前的值
const previousData = {
todos: queryClient.getQueryData(['todos']),
todo: queryClient.getQueryData(['todo', newTodo.id]),
stats: queryClient.getQueryData(['todoStats']),
};
// 更新多个相关查询
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
queryClient.setQueryData(['todo', newTodo.id], newTodo);
queryClient.setQueryData(['todoStats'], (old) => ({
...old,
completed: newTodo.completed
? old.completed + 1
: old.completed - 1,
}));
return previousData;
},
onError: (err, newTodo, context) => {
// 回滚所有更新
queryClient.setQueryData(['todos'], context.previousData.todos);
queryClient.setQueryData(
['todo', newTodo.id],
context.previousData.todo
);
queryClient.setQueryData(['todoStats'], context.previousData.stats);
},
onSettled: () => {
// 无论成功失败,都重新获取以确保数据同步
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todoStats'] });
},
});
}
4. 自定义缓存行为
实现复杂的缓存策略:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 自定义缓存时间计算
cacheTime: (query) => {
if (query.queryKey[0] === 'user') {
return 1000 * 60 * 60; // 用户数据缓存1小时
}
if (query.queryKey[0] === 'post') {
return 1000 * 60 * 5; // 帖子数据缓存5分钟
}
return 1000 * 60 * 30; // 默认缓存30分钟
},
// 自定义过期时间
staleTime: (query) => {
if (query.queryKey[0] === 'config') {
return Infinity; // 配置数据永不过期
}
return 0; // 其他数据立即过期
},
},
},
});
5. 自定义数据转换
在查询和修改时处理复杂的数据转换:
function TransformedData() {
// 查询时转换数据
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => ({
items: data.map(todo => ({
...todo,
displayTitle: `${todo.id}: ${todo.title}`,
isOverdue: new Date(todo.dueDate) < new Date(),
})),
stats: {
total: data.length,
completed: data.filter(t => t.completed).length,
overdue: data.filter(t =>
new Date(t.dueDate) < new Date()
).length,
},
}),
});
// 修改时转换数据
const mutation = useMutation({
mutationFn: (todo) => {
const transformed = {
...todo,
updatedAt: new Date().toISOString(),
version: (todo.version || 0) + 1,
};
return updateTodo(transformed);
},
});
return (
<div>
<div>Total: {data.stats.total}</div>
<div>Completed: {data.stats.completed}</div>
<div>Overdue: {data.stats.overdue}</div>
{data.items.map(todo => (
<div key={todo.id}>
{todo.displayTitle}
{todo.isOverdue && <span>Overdue!</span>}
</div>
))}
</div>
);
}
6. 复杂的重试策略
自定义重试逻辑和错误处理:
function CustomRetryQuery() {
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: (failureCount, error) => {
// 根据错误类型决定是否重试
if (error.status === 404) return false; // 不重试404错误
if (error.status === 401) return false; // 不重试认证错误
// 最多重试3次
return failureCount < 3;
},
retryDelay: (attemptIndex) => {
// 指数退避策略
const baseDelay = 1000; // 1秒
const maxDelay = 30000; // 30秒
const delay = Math.min(
baseDelay * Math.pow(2, attemptIndex),
maxDelay
);
// 添加随机抖动
return delay + Math.random() * 1000;
},
});
}
7. 自定义Hook组 合
创建复合的自定义Hook来处理复杂的数据获取逻辑:
function useUserData(userId) {
// 获取用户基本信息
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// 获取用户权限
const permissionsQuery = useQuery({
queryKey: ['permissions', userId],
queryFn: () => fetchUserPermissions(userId),
enabled: !!userQuery.data,
});
// 获取用户偏好设置
const preferencesQuery = useQuery({
queryKey: ['preferences', userId],
queryFn: () => fetchUserPreferences(userId),
enabled: !!userQuery.data,
});
// 组合所有数据
const combinedData = useMemo(() => {
if (!userQuery.data) return null;
return {
...userQuery.data,
permissions: permissionsQuery.data || [],
preferences: preferencesQuery.data || {},
};
}, [
userQuery.data,
permissionsQuery.data,
preferencesQuery.data,
]);
// 组合加载状态
const isLoading =
userQuery.isLoading ||
permissionsQuery.isLoading ||
preferencesQuery.isLoading;
// 组合错误状态
const error =
userQuery.error ||
permissionsQuery.error ||
preferencesQuery.error;
return {
data: combinedData,
isLoading,
error,
refetch: async () => {
await Promise.all([
userQuery.refetch(),
permissionsQuery.refetch(),
preferencesQuery.refetch(),
]);
},
};
}
// 使用示例
function UserProfile({ userId }) {
const { data, isLoading, error, refetch } = useUserData(userId);
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{data.name}</h1>
<div>
Permissions: {data.permissions.join(', ')}
</div>
<div>
Theme: {data.preferences.theme}
</div>
<button onClick={refetch}>Refresh</button>
</div>
);
}
高级特性
1. 无限查询
// React 示例
function InfiniteTodos() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['todos'],
queryFn: ({ pageParam = 0 }) => fetchTodoPage(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.todos.map(todo => (
<p key={todo.id}>{todo.title}</p>
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</>
);
}
2. 预取数据
// React 示例
const queryClient = useQueryClient();
// 预取数据
await queryClient.prefetchQuery({
queryKey: ['todo', 5],
queryFn: () => fetchTodoById(5),
});
// Vue 示例
const queryClient = useQueryClient();
await queryClient.prefetchQuery(['todo', 5], () => fetchTodoById(5));
3. 并行查询
// React 示例
function ParallelQueries() {
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return (
<div>
{users.data?.map(user => (
<div key={user.id}>{user.name}</div>
))}
{todos.data?.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}
最佳实践
-
合理设置 staleTime 和 cacheTime
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 数据5分钟内保持新鲜
cacheTime: 1000 * 60 * 30, // 未使用的数据缓存30分钟
},
},
}); -
使用 Suspense 模式
// React
function App() {
return (
<Suspense fallback={<Loading />}>
<Todo />
</Suspense>
);
} -
错误处理和重试
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: error => {
console.error('Failed to fetch todos:', error);
},
});