| name | query-patterns |
| description | Implement TanStack Query (React Query) patterns for data fetching, caching, mutations, and optimistic updates. Use when setting up API integration or data fetching strategies. |
| allowed-tools | Read, Write, Grep |
You are a TanStack Query (React Query) patterns expert. You help implement efficient, scalable data fetching patterns with proper TypeScript types and caching strategies.
React Query Pattern Implementations
1. Organized Query Structure
Separate concerns: API calls, query keys, and query hooks.
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export const usersApi = {
getAll: async (params?: GetUsersParams) => {
const { data } = await api.get<User[]>('/users', { params });
return data;
},
getById: async (id: string) => {
const { data } = await api.get<User>(`/users/${id}`);
return data;
},
create: async (user: CreateUserInput) => {
const { data } = await api.post<User>('/users', user);
return data;
},
update: async (id: string, updates: Partial<User>) => {
const { data} = await api.patch<User>(`/users/${id}`, updates);
return data;
},
delete: async (id: string) => {
await api.delete(`/users/${id}`);
},
};
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (params?: GetUsersParams) => [...userKeys.lists(), params] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { usersApi } from '../api/users';
import { userKeys } from './queryKeys';
export function useUsers(params?: GetUsersParams) {
return useQuery({
queryKey: userKeys.list(params),
queryFn: () => usersApi.getAll(params),
staleTime: 5 * 60 * 1000,
});
}
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => usersApi.getById(id),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: usersApi.create,
onSuccess: (newUser) => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
queryClient.setQueryData<User[]>(
userKeys.list(),
(old) => old ? [...old, newUser] : [newUser]
);
},
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, updates }: { id: string; updates: Partial<User> }) =>
usersApi.update(id, updates),
onMutate: async ({ id, updates }) => {
await queryClient.cancelQueries({ queryKey: userKeys.detail(id) });
const previousUser = queryClient.getQueryData<User>(userKeys.detail(id));
queryClient.setQueryData<User>(userKeys.detail(id), (old) => {
if (!old) return old;
return { ...old, ...updates };
});
return { previousUser };
},
onError: (err, { id }, context) => {
if (context?.previousUser) {
queryClient.setQueryData(userKeys.detail(id), context.previousUser);
}
},
onSettled: (data, error, { id }) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: usersApi.delete,
onSuccess: (_, deletedId) => {
queryClient.removeQueries({ queryKey: userKeys.detail(deletedId) });
queryClient.setQueriesData<User[]>(
{ queryKey: userKeys.lists() },
(old) => old?.filter((user) => user.id !== deletedId)
);
},
});
}
2. Infinite Query Pattern (Pagination)
import { useInfiniteQuery } from '@tanstack/react-query';
interface PageResponse<T> {
data: T[];
nextCursor?: string;
hasMore: boolean;
}
export function useInfiniteUsers(params?: GetUsersParams) {
return useInfiniteQuery({
queryKey: userKeys.list(params),
queryFn: ({ pageParam }) =>
usersApi.getAll({ ...params, cursor: pageParam }),
getNextPageParam: (lastPage: PageResponse<User>) => {
return lastPage.hasMore ? lastPage.nextCursor : undefined;
},
initialPageParam: undefined,
});
}
function UserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
error,
} = useInfiniteUsers();
const users = data?.pages.flatMap((page) => page.data) ?? [];
const observerTarget = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 1.0 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
<div ref={observerTarget} />
{isFetchingNextPage && <LoadingSpinner />}
</div>
);
}
3. Dependent Queries
function UserPosts({ userId }: { userId: string }) {
const {
data: user,
isLoading: userLoading,
error: userError,
} = useUser(userId);
const {
data: posts,
isLoading: postsLoading,
} = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => postsApi.getByUserId(user!.id),
enabled: !!user,
});
if (userLoading) return <div>Loading user...</div>;
if (userError) return <div>Error loading user</div>;
if (!user) return <div>User not found</div>;
if (postsLoading) return <div>Loading posts...</div>;
return (
<div>
<h2>{user.name}'s Posts</h2>
{posts?.map((post) => <PostCard key={post.id} post={post} />)}
</div>
);
}
4. Prefetching Pattern
function UserListItem({ user }: { user: User }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: userKeys.detail(user.id),
queryFn: () => usersApi.getById(user.id),
staleTime: 60 * 1000,
});
};
return (
<Link
to={`/users/${user.id}`}
onMouseEnter={prefetchUser}
onFocus={prefetchUser}
>
{user.name}
</Link>
);
}
function PaginatedList({ page }: { page: number }) {
const queryClient = useQueryClient();
const { data } = useUsers({ page });
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: userKeys.list({ page: page + 1 }),
queryFn: () => usersApi.getAll({ page: page + 1 }),
});
}
}, [page, data, queryClient]);
return <div>{/* Render users */}</div>;
}
5. Optimistic Updates with Rollback
export function useToggleLike(postId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (liked: boolean) => postsApi.toggleLike(postId, liked),
onMutate: async (liked) => {
await queryClient.cancelQueries({ queryKey: postKeys.detail(postId) });
const previousPost = queryClient.getQueryData<Post>(
postKeys.detail(postId)
);
queryClient.setQueryData<Post>(postKeys.detail(postId), (old) => {
if (!old) return old;
return {
...old,
liked,
likeCount: old.likeCount + (liked ? 1 : -1),
};
});
return { previousPost };
},
onError: (err, variables, context) => {
if (context?.previousPost) {
queryClient.setQueryData(
postKeys.detail(postId),
context.previousPost
);
}
toast.error('Failed to update like');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: postKeys.detail(postId) });
},
});
}
6. Parallel Queries
function Dashboard() {
const userQuery = useUser('current');
const postsQuery = usePosts({ userId: 'current' });
const notificationsQuery = useNotifications();
if (userQuery.isLoading || postsQuery.isLoading || notificationsQuery.isLoading) {
return <LoadingSpinner />;
}
const error = userQuery.error || postsQuery.error || notificationsQuery.error;
if (error) {
return <ErrorMessage error={error} />;
}
return (
<div>
<UserProfile user={userQuery.data} />
<PostList posts={postsQuery.data} />
<NotificationList notifications={notificationsQuery.data} />
</div>
);
}
function MultiUserView({ userIds }: { userIds: string[] }) {
const userQueries = useQueries({
queries: userIds.map((id) => ({
queryKey: userKeys.detail(id),
queryFn: () => usersApi.getById(id),
})),
});
const isLoading = userQueries.some((q) => q.isLoading);
const users = userQueries.map((q) => q.data).filter(Boolean);
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
7. Mutation with Multiple Cache Updates
export function useUpdateProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (updates: Partial<User>) =>
usersApi.update('current', updates),
onSuccess: (updatedUser) => {
queryClient.setQueryData(userKeys.detail('current'), updatedUser);
queryClient.setQueriesData<User[]>(
{ queryKey: userKeys.lists() },
(old) => {
if (!old) return old;
return old.map((user) =>
user.id === updatedUser.id ? updatedUser : user
);
}
);
queryClient.setQueriesData<Post[]>(
{ queryKey: ['posts'] },
(old) => {
if (!old) return old;
return old.map((post) =>
post.authorId === updatedUser.id
? { ...post, authorName: updatedUser.name }
: post
);
}
);
},
});
}
Best Practices
-
Query Keys:
- Use factory pattern for consistency
- Include all variables affecting the query
- Hierarchical structure for easy invalidation
-
Caching Strategy:
- Set appropriate
staleTime (how long data is fresh)
- Set appropriate
gcTime (how long unused data is cached)
- Use prefetching for better UX
-
Error Handling:
- Always handle errors in UI
- Use error boundaries for critical failures
- Configure retry logic appropriately
-
Performance:
- Use
enabled option to prevent unnecessary requests
- Leverage
select to transform data
- Implement optimistic updates for better UX
-
Type Safety:
- Define proper TypeScript interfaces
- Type query responses
- Type mutation variables
Common Patterns Summary
| Pattern | Use Case |
|---|
| Basic Query | Fetching data |
| Mutation | Creating/updating/deleting data |
| Infinite Query | Pagination/infinite scroll |
| Dependent Queries | Data that depends on other data |
| Prefetching | Improve perceived performance |
| Optimistic Updates | Instant UI feedback |
| Parallel Queries | Fetch multiple resources simultaneously |
This skill helps you implement efficient, scalable data fetching patterns with TanStack Query.