使用 TanStack Query 管理服务器状态(Server State)

三葉Leaves Author

为什么需要

TanStack Query 工作在客户端,用来在客户端管理服务器状态。

服务器状态有其独特的特点:

  • 它存储在远程,我们无法直接控制。

  • 它需要异步获取。

  • 它可能被其他人修改,导致我们本地的数据“过时”(stale)。

  • 它和缓存、重试、数据同步等概念紧密相关。

想象下面这些场景:

  1. 代码冗余:每个需要请求数据的组件,几乎都要重复写一遍 isLoading, error, data 这三个状态以及 useEffect 中的逻辑。

  2. 缺少缓存:如果用户切换到其他页面再切回来,这个组件会重新挂载,useEffect 会再次执行,数据会重新请求。这不仅浪费了用户的流量,也增加了服务器的压力,并且用户会再次看到 Loading… 的状态,体验不佳。

  3. 数据状态不同步:假设你在一个“帖子列表”页面,又在另一个“用户详情”页面的侧边栏显示了同一个用户的帖子。如果用户在详情页删除了一个帖子,列表页的数据是不会自动更新的,因为它们是两个独立的请求和状态。

  4. 复杂的状态管理:当涉及到分页、无限滚动、轮询等高级场景时,useEffect 里的逻辑会变得异常复杂和难以维护。

  5. 后台自动更新困难:当用户长时间停留在页面上时,屏幕上的数据可能已经“过时”了。我们如何能在不打扰用户的情况下,在后台悄悄更新数据呢?用 useEffect + setInterval?这会引入新的复杂性。

TanStack Query 使用 query key 唯一标识我们的 state ,给其提供缓存,使得分散在各处的 fetch 请求可以使用同一份东西,让整个系统联系起来。

另可见:Server Action,TanStack Query … 我到底要用谁

快速上手

安装

1
2
3
pnpm add @tanstack/react-query
pnpm add -D @tanstack/eslint-plugin-query
pnpm add @tanstack/react-query-devtools

定义 Provider

新建一个 src\providers\QueryProvider.tsx:

这里我额外引入了 ReactQueryDevtools,这样做之后浏览器能显示出专属的 dev tool。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";

export default function QueryProvider({
children,
}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

然后再 main.tsx 或者 src\app\layout.tsx 里使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{/* 使用我们的 Provider 包裹应用 */}
<QueryProvider>
{children}
</QueryProvider>
</body>
</html>
);
}

使用示例

组件里使用示例:

核心就是其中的 useQuery hook,这个东西能解构出来一堆有用的玩意,其中的 data 就是我们要的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { useQuery } from '@tanstack/react-query';

// 我们可以把请求逻辑封装成一个独立的函数,更清晰
const fetchTodos = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

function Todos() {
// 这就是 TanStack Query 的魔力所在!
const { data, error, isLoading, isError } = useQuery({
queryKey: ['todos'], // 数据的唯一标识
queryFn: fetchTodos, // 获取数据的函数
});

if (isLoading) {
return <span>Loading...</span>;
}

if (isError) {
// error 对象包含了具体的错误信息
return <span>Error: {error.message}</span>;
}

// 成功获取数据后,data 就有值了
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}

代码组织

社区推荐的方式是把 query options 拆分管理,便于复用,所以我们新建一个像是这样的文件:

src\features\todos\createTodoQueryOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { queryOptions } from "@tanstack/react-query";
import { getTodos } from "./todoServices";
import { Todo } from "./types";

export default function createTodoQueryOptions(initialTodos: Todo[]) {
return queryOptions({
queryKey: ["todos"],
queryFn: getTodos,
initialData: initialTodos,
staleTime: 1000 * 60,
});
}

注意我们这里并不是定义了一个死的对象,而是用了 queryOptions 函数。

之后使用的时候,就可以直接引入了:

src\features\todos\components\todo-manager.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import createTodoQueryOptions from "../createTodoQueryOptions";

type TodoManagerProps = {
initialTodos: Todo[];
};

export function TodoManager({ initialTodos }: TodoManagerProps) {
// 这里就可以直接使用了
const { data, isFetching } = useQuery(createTodoQueryOptions(initialTodos));
return isFetching ? (
<div>Loading...</div>
) : (
<div>
<TodoList todos={data} className="w-sm md:w-md mx-auto gap-4" />
</div>
);
}

使用 useMutation 变更数据

我们使用一个 useMutation hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { mutate } = useMutation({
mutationFn: ( content ) => addTodo( content ),
onMutate: (variables) => {
// A mutation is about to happen!

// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})

该 hook 内部先需要我们定义一个 mutationFn,其实就是我们用来做数据变更的函数。结合 [[server action & useActionState hook | server action]] 使用的话,这里就可以填入一个 server action 函数。

解构出来的 mutate ,其实就是我们下面需要用的东西。

在组件中使用:

1
mutate(content);

相当于使用了我们定义的 mutationFn。

那么,我们为什么不直接使用 fetch 等函数?

我们注意到,除了 mutationFn ,我们还可以定义一系列回调函数。

这使得我们可以在突变生命周期的任何阶段快速轻松地执行副作用。这些选项在以下场景中会很有用:

  • 突变后使缓存失效
  • 重新获取查询
  • 乐观更新

不过,如果你不需要在 mutation 进行的生命周期中做什么事,那也确实不需要用 useMutation 了。

我们大可以直接用一遍封装好的 fetch 函数,然后手动执行一次缓存失效

1
queryClient.invalidateQueries({ queryKey: ['todos'] })
  • 标题: 使用 TanStack Query 管理服务器状态(Server State)
  • 作者: 三葉Leaves
  • 创建于 : 2025-09-08 00:00:00
  • 更新于 : 2025-09-26 18:04:55
  • 链接: https://blog.oksanye.com/63dc31fb68e4/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论