Query Hooks
Custom hooks for data fetching with TanStack Query.
Basic Query Hook
// features/users/hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/constants/queryKeys';
import { userService } from '../services/userService';
import type { User, UserFilters } from '../types';
export const useUsers = (filters?: UserFilters) => {
return useQuery({
queryKey: queryKeys.users.list(filters ?? {}),
queryFn: () => userService.getUsers(filters),
});
};Single Item Query
export const useUser = (id: string | undefined) => {
return useQuery({
queryKey: queryKeys.users.detail(id!),
queryFn: () => userService.getUser(id!),
enabled: !!id, // Only run when id exists
});
};Infinite Query Hook
import { useInfiniteQuery } from '@tanstack/react-query';
export const useInfiniteUsers = (filters?: UserFilters) => {
return useInfiniteQuery({
queryKey: queryKeys.users.list({ ...filters, infinite: true }),
queryFn: ({ pageParam = 1 }) =>
userService.getUsersPaginated({
page: pageParam,
pageSize: 20,
...filters,
}),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const { page, totalPages } = lastPage.pagination;
return page < totalPages ? page + 1 : undefined;
},
});
};
// Usage
const UserList = () => {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteUsers();
const users = data?.pages.flatMap((page) => page.data) ?? [];
return (
<>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
{hasNextPage && (
<Button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</Button>
)}
</>
);
};Mutation Hooks
Create Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.createUser,
onSuccess: (newUser) => {
queryClient.invalidateQueries({
queryKey: queryKeys.users.lists(),
});
toast.success('User created successfully');
},
onError: (error: Error) => {
toast.error(error.message || 'Failed to create user');
},
});
};Update Mutation
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserDTO }) =>
userService.updateUser(id, data),
onSuccess: (updatedUser, { id }) => {
// Update the cache directly
queryClient.setQueryData(
queryKeys.users.detail(id),
updatedUser
);
// Invalidate the list
queryClient.invalidateQueries({
queryKey: queryKeys.users.lists(),
});
toast.success('User updated');
},
});
};Delete Mutation
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.deleteUser,
onSuccess: (_, deletedId) => {
// Remove from list cache
queryClient.setQueryData(
queryKeys.users.lists(),
(old: User[] = []) => old.filter((u) => u.id !== deletedId)
);
toast.success('User deleted');
},
});
};Prefetching
// Prefetch on hover
const UserListItem = ({ user }: { user: User }) => {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.users.detail(user.id),
queryFn: () => userService.getUser(user.id),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
return (
<Link
to={`/users/${user.id}`}
onMouseEnter={prefetchUser}
>
{user.name}
</Link>
);
};Dependent Queries
const useUserWithPosts = (userId: string) => {
const userQuery = useUser(userId);
const postsQuery = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => postService.getPostsByUser(userId),
enabled: !!userQuery.data, // Only fetch when user is loaded
});
return {
user: userQuery.data,
posts: postsQuery.data,
isLoading: userQuery.isLoading || postsQuery.isLoading,
};
};Parallel Queries
import { useQueries } from '@tanstack/react-query';
const useMultipleUsers = (ids: string[]) => {
return useQueries({
queries: ids.map((id) => ({
queryKey: queryKeys.users.detail(id),
queryFn: () => userService.getUser(id),
})),
});
};