TanStack Query v5
Quick start
npm install @tanstack/react-query
or
yarn add @tanstack/react-query
or
pnpm add @tanstack/react-query
// providers.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ); }
// Basic usage import { useQuery } from "@tanstack/react-query";
function Users() { const { data, isLoading, error } = useQuery({ queryKey: ["users"], queryFn: () => fetch("/api/users").then((res) => res.json()), });
if (isLoading) return "Loading..."; if (error) return "An error occurred";
return <div>{JSON.stringify(data)}</div>; }
Common patterns
Data fetching with loading states
function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading, error, } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId), enabled: !!userId, });
if (isLoading) return <Skeleton />; if (error) return <ErrorMessage error={error} />; if (!user) return null;
return <UserCard user={user} />; }
Mutations with optimistic updates
function LikeButton({ postId }: { postId: string }) { const queryClient = useQueryClient();
const mutation = useMutation({ mutationFn: () => toggleLike(postId), onMutate: async () => { await queryClient.cancelQueries({ queryKey: ["posts"] }); const previousPosts = queryClient.getQueryData(["posts"]); queryClient.setQueryData(["posts"], (old: any[]) => old?.map((post) => post.id === postId ? { ...post, likes: post.likes + 1 } : post, ), ); return { previousPosts }; }, onError: (err, _, context) => { queryClient.setQueryData(["posts"], context?.previousPosts); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ["posts"] }); }, });
return ( <button onClick={() => mutation.mutate()}> ❤️ {mutation.isPending ? "..." : "Like"} </button> ); }
Pagination
function PaginatedPosts() { const [page, setPage] = useState(1);
const { data, isLoading, isPlaceholderData } = useQuery({ queryKey: ["posts", page], queryFn: () => fetchPosts(page), placeholderData: keepPreviousData, });
return ( <div> {data?.posts.map((post) => ( <Post key={post.id} post={post} /> ))} <button onClick={() => setPage((p) => p - 1)} disabled={page === 1 || isLoading} > Previous </button> <button onClick={() => setPage((p) => p + 1)} disabled={!data?.hasNextPage || isLoading} > Next </button> </div> ); }
Infinite scroll
function InfinitePosts() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ["posts"], queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam), getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, });
return ( <div> {data?.pages.map((page) => page.posts.map((post) => <Post key={post.id} post={post} />), )} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? "Loading more..." : "Load more"} </button> </div> ); }
Dependent queries
function UserProfile({ userId }: { userId: string }) { const { data: user } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId), });
const { data: posts } = useQuery({ queryKey: ["posts", userId], queryFn: () => fetchUserPosts(userId), enabled: !!user?.id, });
return ( <div> <h1>{user?.name}</h1> {posts?.map((post) => ( <Post key={post.id} post={post} /> ))} </div> ); }
Requirements
Installation
Core package
npm install @tanstack/react-query
DevTools (recommended for development)
npm install @tanstack/react-query-devtools
Browser support
-
Supports all modern browsers
-
IE11+ with appropriate polyfills
React version compatibility
-
React 16.8+ (hooks required)
-
React 18+ preferred for concurrent features
TypeScript support
-
Built-in TypeScript definitions
-
Full type inference for queries and mutations