tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query Best Practices
TanStack Query 最佳实践
Server state management with automatic caching and synchronization.
具备自动缓存与同步功能的服务端状态管理方案。
Instructions
操作指南
1. Setup
1. 环境搭建
tsx
// providers/QueryProvider.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 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}tsx
// providers/QueryProvider.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 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}2. Query Keys Factory
2. 查询键工厂
typescript
// lib/queryKeys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
list: (filters: UserFilters) => [...queryKeys.users.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.users.all, 'detail', id] as const,
},
posts: {
all: ['posts'] as const,
list: (filters: PostFilters) => [...queryKeys.posts.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.posts.all, 'detail', id] as const,
byUser: (userId: string) => [...queryKeys.posts.all, 'user', userId] as const,
},
} as const;typescript
// lib/queryKeys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
list: (filters: UserFilters) => [...queryKeys.users.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.users.all, 'detail', id] as const,
},
posts: {
all: ['posts'] as const,
list: (filters: PostFilters) => [...queryKeys.posts.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.posts.all, 'detail', id] as const,
byUser: (userId: string) => [...queryKeys.posts.all, 'user', userId] as const,
},
} as const;3. Custom Query Hook
3. 自定义查询Hook
typescript
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
import { userApi } from '@/lib/api';
export function useUsers(filters?: UserFilters) {
return useQuery({
queryKey: queryKeys.users.list(filters ?? {}),
queryFn: () => userApi.getAll(filters),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: queryKeys.users.detail(id),
queryFn: () => userApi.getById(id),
enabled: !!id, // Only run when id exists
});
}typescript
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
import { userApi } from '@/lib/api';
export function useUsers(filters?: UserFilters) {
return useQuery({
queryKey: queryKeys.users.list(filters ?? {}),
queryFn: () => userApi.getAll(filters),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: queryKeys.users.detail(id),
queryFn: () => userApi.getById(id),
enabled: !!id, // Only run when id exists
});
}4. Mutations
4. Mutations 操作
typescript
// hooks/useCreateUser.ts
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserDto) => userApi.create(data),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
onError: (error) => {
toast.error(error.message);
},
});
}
// Usage
function CreateUserForm() {
const { mutate, isPending } = useCreateUser();
const handleSubmit = (data: CreateUserDto) => {
mutate(data, {
onSuccess: () => toast.success('User created!'),
});
};
}typescript
// hooks/useCreateUser.ts
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserDto) => userApi.create(data),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
onError: (error) => {
toast.error(error.message);
},
});
}
// Usage
function CreateUserForm() {
const { mutate, isPending } = useCreateUser();
const handleSubmit = (data: CreateUserDto) => {
mutate(data, {
onSuccess: () => toast.success('User created!'),
});
};
}5. Optimistic Updates
5. 乐观更新
typescript
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) =>
userApi.update(id, data),
onMutate: async ({ id, data }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.users.detail(id) });
// Snapshot previous value
const previousUser = queryClient.getQueryData(queryKeys.users.detail(id));
// Optimistically update
queryClient.setQueryData(queryKeys.users.detail(id), (old: User) => ({
...old,
...data,
}));
return { previousUser };
},
onError: (err, { id }, context) => {
// Rollback on error
queryClient.setQueryData(
queryKeys.users.detail(id),
context?.previousUser
);
},
onSettled: (_, __, { id }) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
},
});
}typescript
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) =>
userApi.update(id, data),
onMutate: async ({ id, data }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.users.detail(id) });
// Snapshot previous value
const previousUser = queryClient.getQueryData(queryKeys.users.detail(id));
// Optimistically update
queryClient.setQueryData(queryKeys.users.detail(id), (old: User) => ({
...old,
...data,
}));
return { previousUser };
},
onError: (err, { id }, context) => {
// Rollback on error
queryClient.setQueryData(
queryKeys.users.detail(id),
context?.previousUser
);
},
onSettled: (_, __, { id }) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
},
});
}6. Infinite Query (Pagination)
6. 无限查询(分页)
typescript
export function useInfinitePosts() {
return useInfiniteQuery({
queryKey: queryKeys.posts.all,
queryFn: ({ pageParam = 1 }) => postApi.getAll({ page: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
initialPageParam: 1,
});
}
// Usage
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</>
);
}typescript
export function useInfinitePosts() {
return useInfiniteQuery({
queryKey: queryKeys.posts.all,
queryFn: ({ pageParam = 1 }) => postApi.getAll({ page: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
initialPageParam: 1,
});
}
// Usage
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</>
);
}7. Prefetching
7. 预加载
typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => userApi.getById(userId),
staleTime: 1000 * 60 * 5,
});
};
return (
<Link href={`/users/${userId}`} onMouseEnter={prefetchUser}>
View User
</Link>
);
}typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => userApi.getById(userId),
staleTime: 1000 * 60 * 5,
});
};
return (
<Link href={`/users/${userId}`} onMouseEnter={prefetchUser}>
View User
</Link>
);
}8. Dependent Queries
8. 依赖查询
typescript
function useUserPosts(userId: string | undefined) {
const userQuery = useUser(userId!);
return useQuery({
queryKey: queryKeys.posts.byUser(userId!),
queryFn: () => postApi.getByUser(userId!),
enabled: !!userId && userQuery.isSuccess,
});
}typescript
function useUserPosts(userId: string | undefined) {
const userQuery = useUser(userId!);
return useQuery({
queryKey: queryKeys.posts.byUser(userId!),
queryFn: () => postApi.getByUser(userId!),
enabled: !!userId && userQuery.isSuccess,
});
}Common Patterns
常见模式
| Pattern | Usage |
|---|---|
| How long data stays fresh |
| How long unused data stays in cache |
| Conditional fetching |
| Transform response data |
| Show while loading |
| 模式 | 用途 |
|---|---|
| 数据保持新鲜的时长 |
| 未使用数据在缓存中保留的时长 |
| 条件式数据获取 |
| 转换响应数据 |
| 加载时展示 |