0%

TanStack React Query 使用——数据获取与缓存管理

重构项目的数据获取逻辑,从原来的手动管理切换到了 TanStack Query (原 React Query),使用体验相当棒!自动缓存、智能重试、后台更新等功能让数据管理变得简单高效。分

TanStack Query 简介

  TanStack Query (原名 React Query)是一个强大的数据获取和状态管理库,专门用于处理服务端数据。它提供了自动缓存、后台更新、数据预取、错误处理等开箱即用的功能,大大简化了异步数据的管理。

  随着 TanStack 的成立,React Query 正式更名为 TanStack Query,支持更多的框架,包括 React、Vue、Solid、Svelte 等,但仍保留了 React Query 的核心功能。

核心特性

  1. 自动缓存: 智能的数据缓存和失效机制
  2. 后台数据更新: 页面不可见时自动更新数据
  3. 请求去重: 避免重复的相同请求
  4. 错误处理和重试: 完善的错误处理机制
  5. 乐观更新: 提升用户体验的优化策略
  6. 分页和无限滚动: 简化复杂数据获取场景

安装和基础配置

安装 TanStack Query

1
2
3
4
# 安装核心库和 React 绑定 npm install @tanstack/React-query

# 或使用 yarn
yarn add @tanstack/React-query

基础配置

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
// App.JS
import React from 'React';
import { QueryClient, QueryClientProvider } from '@tanstack/React-query';
import { ReactQueryDevtools } from '@tanstack/React-query/devtools';

// 创建 Query Client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 默认查询选项 staleTime: 1000 * 60 * 5, // 5分钟内认为数据新鲜 cacheTime: 1000 * 60 * 10, // 缓存10分钟 retry: 3, // 失败重试3次 retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避 refetchOnWindowFocus: true, // 窗口聚焦时重新获取 refetchOnReconnect: true, // 重连时重新获取 refetchOnMount: false, // 挂载时重新获取
},
mutations: {
// 默认突变选项 retry: 1,
}
}
});

function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
{/* 你的应用组件 */}
</div>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

export default App;

核心 Hooks 使用

useQuery 基础使用

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
import { useQuery } from '@tanstack/React-query';

// 基础查询 function UserList() {
const { data, isLoading, isError, error, isFetching } = useQuery({
queryKey: ['users'], // 查询的唯一标识 queryFn: async () => {
const response = await fetch('/API/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.Json();
}
});

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;

return (
<div>
<h2>Users ({isFetching ? 'updating...' : ''})</h2>
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

useQuery 高级配置

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { useQuery } from '@tanstack/React-query';

function UserProfile({ userId }) {
const query = useQuery({
queryKey: ['user', userId], // 带参数的查询键 queryFn: async ({ signal }) => {
const response = await fetch(`/API/users/${userId}`, { signal });

if (!response.ok) {
if (response.status === 404) {
throw new Error('User not found');
}
throw new Error(`Failed to fetch user: ${response.status}`);
}

return response.Json();
},

// 查询选项 enabled: !!userId, // 只有 userId 存在时才执行查询 staleTime: 1000 * 60 * 2, // 2分钟后数据变为陈旧 cacheTime: 1000 * 60 * 5, // 5分钟缓存 refetchInterval: false, // 不自动轮询 refetchOnWindowFocus: true, // 窗口聚焦时重新获取 retry: 2, // 重试2次 retryDelay: 1000, // 每次重试间隔1秒

// 成功回调 onSuccess: (data) => {
console.log('User fetched successfully:', data);
// 可以在这里执行其他操作
},

// 错误回调 onError: (error) => {
console.error('Failed to fetch user:', error);
// 错误处理逻辑
},

// 转换数据 select: (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`,
isActive: data.status === 'active'
})
});

return (
<div>
{query.isLoading && <div>Loading user...</div>}
{query.isError && (
<div>
Error: {query.error.message}
<button onClick={() => query.refetch()}>Retry</button>
</div>
)}
{query.data && (
<div>
<h3>{query.data.fullName}</h3>
<p>Status: {query.data.isActive ? 'Active' : 'Inactive'}</p>
</div>
)}
</div>
);
}

useQueries 并行查询

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
37
38
39
40
41
42
import { useQueries } from '@tanstack/React-query';

function Dashboard() {
const results = useQueries({
queries: [
{
queryKey: ['users'],
queryFn: () => fetch('/API/users').then(res => res.Json()),
staleTime: 1000 * 60 * 5
},
{
queryKey: ['posts'],
queryFn: () => fetch('/API/posts').then(res => res.Json()),
staleTime: 1000 * 60 * 3
},
{
queryKey: ['comments'],
queryFn: () => fetch('/API/comments').then(res => res.Json()),
enabled: false // 需要手动启用
}
]
});

const [users, posts, comments] = results;

if (results.some(result => result.isLoading)) {
return <div>Loading dashboard...</div>;
}

return (
<div>
<section>
<h3>Users: {users.data?.length}</h3>
<button onClick={() => users.refetch()}>Refresh Users</button>
</section>
<section>
<h3>Posts: {posts.data?.length}</h3>
<button onClick={() => posts.refetch()}>Refresh Posts</button>
</section>
</div>
);
}

数据突变 (Mutations)

useMutation 基础使用

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { useMutation, useQueryClient } from '@tanstack/React-query';

function CreateUserForm() {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: async (userData) => {
const response = await fetch('/API/users', {
method: 'POST',
headers: {
'Content-Type': 'application/Json',
},
body: Json.stringify(userData),
});

if (!response.ok) {
throw new Error('Failed to create user');
}

return response.Json();
},

// 成功回调 - 自动失效相关缓存 onSuccess: (newUser) => {
// 使用户列表查询失效,下次访问时会重新获取 queryClient.invalidateQueries({ queryKey: ['users'] });

// 或者直接更新缓存 queryClient.setQueryData(['users'], (oldUsers) => {
return [...oldUsers, newUser];
});

// 显示成功消息 alert('User created successfully!');
},

// 错误回调 onError: (error) => {
console.error('Create user failed:', error);
alert(`Failed to create user: ${error.message}`);
},

// 在突变开始前执行 onMutate: async (newUser) => {
// 取消相关查询以避免竞争条件 await queryClient.cancelQueries({ queryKey: ['users'] });

// 保存之前的用户数据 const previousUsers = queryClient.getQueryData(['users']);

// 乐观更新 queryClient.setQueryData(['users'], (oldUsers = []) => [
...oldUsers,
{ id: Date.now(), ...newUser, status: 'optimistic' }
]);

// 返回回滚函数 return { previousUsers };
},

// 错误时的清理 onError: (err, newUser, context) => {
// 错误时回滚到之前的缓存数据 if (context?.previousUsers) {
queryClient.setQueryData(['users'], context.previousUsers);
}
}
});

const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = {
name: formData.get('name'),
email: formData.get('email')
};

mutation.mutate(userData);
};

return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && (
<div style={{ color: 'red' }}>
Error: {mutation.error.message}
</div>
)}
</form>
);
}

useMutation 高级模式

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
function UpdateUserForm({ userId, initialData }) {
const queryClient = useQueryClient();

const updateMutation = useMutation({
mutationFn: async ({ id, data }) => {
const response = await fetch(`/API/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/Json' },
body: Json.stringify(data)
});

if (!response.ok) {
throw new Error('Failed to update user');
}

return response.Json();
},

// 乐观更新 onMutate: async (updatedData) => {
await queryClient.cancelQueries({ queryKey: ['user', userId] });

const previousData = queryClient.getQueryData(['user', userId]);

// 立即更新缓存以获得更好的用户体验 queryClient.setQueryData(['user', userId], {
...previousData,
...updatedData.data,
status: 'updating'
});

return { previousData };
},

onError: (err, updatedData, context) => {
// 错误时恢复数据 if (context?.previousData) {
queryClient.setQueryData(['user', userId], context.previousData);
}
alert(`Update failed: ${err.message}`);
},

onSuccess: (data, variables) => {
// 成功后使相关查询失效 queryClient.invalidateQueries({ queryKey: ['users'] });
alert('User updated successfully!');
}
});

const deleteMutation = useMutation({
mutationFn: async (id) => {
const response = await fetch(`/API/users/${id}`, {
method: 'DELETE'
});

if (!response.ok) {
throw new Error('Failed to delete user');
}

return response.Json();
},

// 删除时的乐观更新 onMutate: async (deletedUserId) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
await queryClient.cancelQueries({ queryKey: ['user', deletedUserId] });

const previousUsers = queryClient.getQueryData(['users']);
const previousUser = queryClient.getQueryData(['user', deletedUserId]);

// 从缓存中移除用户 queryClient.setQueryData(['users'], oldUsers =>
oldUsers?.filter(user => user.id !== deletedUserId) || []
);

// 标记单个用户查询为删除状态 queryClient.setQueryData(['user', deletedUserId], null);

return { previousUsers, previousUser };
},

onError: (err, deletedUserId, context) => {
if (context?.previousUsers) {
queryClient.setQueryData(['users'], context.previousUsers);
}
if (context?.previousUser) {
queryClient.setQueryData(['user', deletedUserId], context.previousUser);
}
alert(`Delete failed: ${err.message}`);
},

onSuccess: () => {
alert('User deleted successfully!');
}
});

const handleUpdate = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = {
name: formData.get('name'),
email: formData.get('email')
};

updateMutation.mutate({ id: userId, data: userData });
};

return (
<div>
<form onSubmit={handleUpdate}>
<input name="name" defaultValue={initialData.name} required />
<input name="email" defaultValue={initialData.email} required />
<button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending ? 'Updating...' : 'Update'}
</button>
</form>

<button
onClick={() => deleteMutation.mutate(userId)}
disabled={deleteMutation.isPending}
style={{ marginLeft: '10px' }}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</div>
);
}

高级功能

无限滚动查询

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { useInfiniteQuery } from '@tanstack/React-query';

function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/API/posts?page=${pageParam}&limit=10`);
const result = await response.Json();
return result;
},
getNextPageParam: (lastPage, allPages) => {
// 如果还有更多数据,返回下一页的页码 return lastPage.hasMore ? allPages.length + 1 : undefined;
},
initialPageParam: 1,
});

if (status === 'pending') return <div>Loading...</div>;
if (status === 'error') return <div>Error loading posts</div>;

const posts = data.pages.flatMap(page => page.posts);

return (
<div>
<div>
{posts.map(post => (
<div key={post.id} style={{ marginBottom: '20px', padding: '10px', border: '1px solid #ccc' }}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>

<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>

<div>{isFetching && !isFetchingNextPage ? 'Background updating...' : null}</div>
</div>
);
}

查询预取 (Prefetching)

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
37
38
39
40
import { useQueryClient } from '@tanstack/React-query';

function PostList({ posts }) {
const queryClient = useQueryClient();

const prefetchPost = (postId) => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`/API/posts/${postId}`).then(res => res.Json()),
staleTime: 1000 * 60 * 5, // 5分钟内不重新获取
});
};

return (
<div>
{posts.map(post => (
<div
key={post.id}
onMouseEnter={() => prefetchPost(post.id)} // 鼠标悬停时预取
>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</div>
))}
</div>
);
}

// 在路由组件中预取数据 function PostDetail({ postId }) {
const queryClient = useQueryClient();
const { data: post, isLoading } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`/API/posts/${postId}`).then(res => res.Json()),
});

return (
<div>
{isLoading ? <div>Loading...</div> : <h1>{post.title}</h1>}
</div>
);
}

查询同步 (Query Invalidation)

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
37
38
39
40
import { useQueryClient } from '@tanstack/React-query';

function CommentSection({ postId }) {
const queryClient = useQueryClient();

const { data: comments, isLoading } = useQuery({
queryKey: ['comments', postId],
queryFn: () => fetch(`/API/posts/${postId}/comments`).then(res => res.Json()),
});

const addCommentMutation = useMutation({
mutationFn: async (commentData) => {
const response = await fetch(`/API/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/Json' },
body: Json.stringify(commentData)
});
return response.Json();
},
onSuccess: () => {
// 添加评论后使缓存失效,重新获取 queryClient.invalidateQueries({ queryKey: ['comments', postId] });

// 也可以使整个评论相关的查询失效 queryClient.invalidateQueries({ queryKey: ['comments'] });
}
});

return (
<div>
<h3>Comments</h3>
{isLoading ? <div>Loading comments...</div> : (
<div>
{comments?.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
</div>
)}
<AddCommentForm onSubmit={addCommentMutation.mutate} />
</div>
);
}

缓存管理

手动缓存操作

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
37
38
39
import { useQueryClient } from '@tanstack/React-query';

function CacheManager() {
const queryClient = useQueryClient();

// 获取缓存数据 const getCachedUsers = () => {
const cachedData = queryClient.getQueryData(['users']);
console.log('Cached users:', cachedData);
return cachedData;
};

// 设置缓存数据 const setCachedUsers = (users) => {
queryClient.setQueryData(['users'], users);
};

// 删除缓存数据 const removeCachedUsers = () => {
queryClient.removeQueries({ queryKey: ['users'] });
};

// 清空所有缓存 const clearAllCache = () => {
queryClient.clear();
};

// 搜索特定查询 const findUserQueries = () => {
const userQueries = queryClient.getQueryCache().findAll({
queryKey: ['user']
});
console.log('User queries:', userQueries);
};

return (
<div>
<button onClick={getCachedUsers}>Get Cached Users</button>
<button onClick={removeCachedUsers}>Remove Cached Users</button>
<button onClick={clearAllCache}>Clear All Cache</button>
<button onClick={findUserQueries}>Find User Queries</button>
</div>
);
}

错误边界和错误处理

自定义错误处理组件

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { QueryErrorResetBoundary } from '@tanstack/React-query';
import { ErrorBoundary } from 'React-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}

function PostsPage() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={ErrorFallback}
>
<PostList />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}

function PostList() {
const { data, error, isLoading, isError, refetch } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/API/posts').then(res => res.Json()),
retry: 1,
retryDelay: 1000,
});

if (isLoading) return <div>Loading posts...</div>;

if (isError) {
throw error; // 抛出错误让错误边界捕获
}

return (
<div>
{data?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}

实际应用场景

用户管理应用

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import React, { useState } from 'React';
import { useQuery, useMutation, useQueryClient } from '@tanstack/React-query';

// 用户服务 const userService = {
getUsers: async () => {
const response = await fetch('/API/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.Json();
},

getUserById: async (id) => {
const response = await fetch(`/API/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.Json();
},

createUser: async (userData) => {
const response = await fetch('/API/users', {
method: 'POST',
headers: { 'Content-Type': 'application/Json' },
body: Json.stringify(userData)
});
if (!response.ok) throw new Error('Failed to create user');
return response.Json();
},

updateUser: async (id, userData) => {
const response = await fetch(`/API/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/Json' },
body: Json.stringify(userData)
});
if (!response.ok) throw new Error('Failed to update user');
return response.Json();
},

deleteUser: async (id) => {
const response = await fetch(`/API/users/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete user');
return response.Json();
}
};

// 用户管理组件 function UserManagement() {
const queryClient = useQueryClient();
const [editingUser, setEditingUser] = useState(null);

// 获取用户列表 const {
data: users,
isLoading,
isError,
error
} = useQuery({
queryKey: ['users'],
queryFn: userService.getUsers,
staleTime: 1000 * 60 * 5, // 5分钟内数据不过期 cacheTime: 1000 * 60 * 10, // 10分钟缓存
});

// 删除用户突变 const deleteMutation = useMutation({
mutationFn: userService.deleteUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});

// 处理删除 const handleDelete = (userId) => {
if (window.confirm('Are you sure you want to delete this user?')) {
deleteMutation.mutate(userId);
}
};

if (isLoading) return <div>Loading users...</div>;
if (isError) return <div>Error: {error.message}</div>;

return (
<div>
<h2>User Management</h2>

<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users?.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<button onClick={() => setEditingUser(user)}>
Edit
</button>
<button
onClick={() => handleDelete(user.id)}
disabled={deleteMutation.isPending}
style={{ marginLeft: '5px' }}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</td>
</tr>
))}
</tbody>
</table>

{editingUser && (
<EditUserModal
user={editingUser}
onClose={() => setEditingUser(null)}
/>
)}
</div>
);
}

// 编辑用户模态框 function EditUserModal({ user, onClose }) {
const queryClient = useQueryClient();

const updateMutation = useMutation({
mutationFn: (userData) => userService.updateUser(user.id, userData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', user.id] });
onClose();
}
});

const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = {
name: formData.get('name'),
email: formData.get('email')
};
updateMutation.mutate(userData);
};

return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px' }}>
<h3>Edit User</h3>
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input name="name" defaultValue={user.name} required />
</div>
<div>
<label>Email:</label>
<input name="email" defaultValue={user.email} required />
</div>
<button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</button>
<button type="button" onClick={onClose} style={{ marginLeft: '10px' }}>
Cancel
</button>
</form>
{updateMutation.isError && (
<div style={{ color: 'red' }}>
Error: {updateMutation.error.message}
</div>
)}
</div>
</div>
);
}

性能优化

查询键的最佳实践

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
// 好的查询键设计 const queryKeys = {
// 基础查询 users: ['users'],

// 带参数的查询 user: (userId) => ['users', userId],
userPosts: (userId) => ['users', userId, 'posts'],

// 带过滤条件的查询 filteredUsers: (filters) => ['users', { filters }],

// 分页查询 paginatedUsers: (page, limit) => ['users', { page, limit }],

// 搜索查询 searchUsers: (searchTerm) => ['users', 'search', searchTerm],
};

// 使用示例 function UserComponent({ userId, searchTerm }) {
// 根据参数动态构建查询键 const { data: user } = useQuery({
queryKey: queryKeys.user(userId),
queryFn: () => fetchUser(userId),
enabled: !!userId
});

const { data: searchResults } = useQuery({
queryKey: queryKeys.searchUsers(searchTerm),
queryFn: () => searchUsers(searchTerm),
enabled: searchTerm && searchTerm.length > 2
});

return (
<div>
{user && <UserProfile user={user} />}
{searchResults && <SearchResults results={searchResults} />}
</div>
);
}

缓存策略优化

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
function OptimizedQueries() {
// 对于实时性要求高的数据 const liveData = useQuery({
queryKey: ['live-data'],
queryFn: fetchLiveData,
staleTime: 0, // 立即过期 cacheTime: 1000 * 60 * 5, // 缓存5分钟 refetchInterval: 1000, // 每秒刷新
});

// 对于不常变化的数据 const staticData = useQuery({
queryKey: ['static-data'],
queryFn: fetchStaticData,
staleTime: 1000 * 60 * 60 * 24, // 24小时后过期 cacheTime: 1000 * 60 * 60 * 48, // 缓存48小时 gcTime: 1000 * 60 * 60 * 72, // 72小时后垃圾回收
});

// 对于大型列表数据 const largeList = useQuery({
queryKey: ['large-list'],
queryFn: fetchLargeList,
staleTime: 1000 * 60 * 10, // 10分钟后过期 cacheTime: 1000 * 60 * 30, // 缓存30分钟 placeholderData: [], // 占位数据 initialData: getLocalStoredList(), // 初始数据 initialDataUpdatedAt: getLocalStoredTimestamp(), // 初始数据更新时间
});

return (
<div>
{/* 渲染组件 */}
</div>
);
}

最佳实践

1. 查询键命名规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 推荐的查询键命名 const queryKeys = {
// 单数形式用于单个实体 user: (id) => ['user', id],

// 复数形式用于列表 users: ['users'],

// 按功能分组 dashboard: {
stats: ['dashboard', 'stats'],
recentActivity: ['dashboard', 'recent-activity'],
},

// 包含上下文信息 project: {
list: (teamId) => ['projects', { teamId }],
detail: (projectId) => ['projects', projectId],
members: (projectId) => ['projects', projectId, 'members'],
}
};

2. 错误处理策略

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
37
38
39
40
41
42
43
44
45
46
47
48
49
function RobustQuery() {
const query = useQuery({
queryKey: ['important-data'],
queryFn: async () => {
const response = await fetch('/API/critical-data');

if (!response.ok) {
// 根据不同错误类型返回不同处理 if (response.status === 404) {
throw new Error('Resource not found', { cause: 'NOT_FOUND' });
} else if (response.status >= 500) {
throw new Error('Server error', { cause: 'SERVER_ERROR' });
} else {
throw new Error('Request failed', { cause: 'CLIENT_ERROR' });
}
}

return response.Json();
},
retry: (failureCount, error) => {
// 根据错误类型决定是否重试 if (error.cause === 'NOT_FOUND') {
return false; // 404错误不重试
}
return failureCount < 3; // 其他错误最多重试3次
},
retryDelay: (attemptIndex) => {
// 指数退避 return Math.min(1000 * 2 ** attemptIndex, 30000);
},
onError: (error) => {
// 记录错误日志 console.error('Query failed:', error);

// 发送到错误监控服务 if (typeof reportError !== 'undefined') {
reportError(error);
}
}
});

return (
<div>
{query.isLoading && <div>Loading...</div>}
{query.isError && (
<div>
Error: {query.error.message}
<button onClick={() => query.refetch()}>Retry</button>
</div>
)}
{query.data && <div>Data: {Json.stringify(query.data)}</div>}
</div>
);
}

总结

  • TanStack Query 提供了完整的服务器状态管理解决方案
  • 自动缓存、后台更新等功能大大简化了数据管理
  • 丰富的 API 支持各种复杂的使用场景
  • 优秀的错误处理和重试机制
  • 与 React 生态完美集成
  • 高性能和良好的开发者体验

使用 TanStack Query 后,数据获取相关的代码量减少了好多,而且缓存和错误处理都非常可靠。特别是乐观更新功能,让用户体验提升了不少。

扩展阅读

  • TanStack Query Documentation
  • React Query Official Guide
  • Server State Management Patterns
  • Query Keys Best Practices
  • Optimistic Updates Guide

参考资料

  • TanStack Query: https://tanstack.com/query
  • React Query GitHub: https://github.com/TanStack/query
  • State Management Solutions: https://github.com/ooade/awesome-React-state-management
  • Data Fetching Patterns: https://www.patterns.dev/posts/client-side-data-fetching/
bulb