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>
);
}