TanStack Query (formerly React Query) handles server state management with caching, background updates, and stale data handling — all automatically. Its API eliminates most of the boilerplate around data fetching.
Basic Queries
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes garbage collection
refetchOnWindowFocus: true,
retry: 3
})
if (isLoading) return <Spinner />
if (error) return <Error message={error.message} />
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
Dependent Queries
function UserPosts({ userId }) {
const userQuery = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId)
})
const postsQuery = useQuery({
queryKey: ["posts", userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data // Only fetch when user is loaded
})
return (
<div>
<h1>{userQuery.data?.name}</h1>
{postsQuery.data?.map(post => <PostCard key={post.id} post={post} />)}
</div>
)
}
Mutations with Optimistic Updates
function CreatePost() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newPost) => fetch("/api/posts", {
method: "POST",
body: JSON.stringify(newPost)
}).then(r => r.json()),
// Optimistic update
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ["posts"] })
const previous = queryClient.getQueryData(["posts"])
queryClient.setQueryData(["posts"], (old) => [
...old,
{ ...newPost, id: "temp-" + Date.now() }
])
return { previous }
},
onError: (err, newPost, context) => {
queryClient.setQueryData(["posts"], context.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] })
}
})
return (
<form onSubmit={(e) => {
e.preventDefault()
mutation.mutate({ title: e.target.title.value })
}}>
<input name="title" />
<button disabled={mutation.isPending}>Create</button>
</form>
)
}
Infinite Queries
function InfiniteList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["posts"],
queryFn: ({ pageParam = 1 }) =>
fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length + 1 : undefined,
initialPageParam: 1
})
return (
<div>
{data.pages.map(page =>
page.items.map(item => <Card key={item.id} item={item} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
</div>
)
}
Prefetching
function PostList() {
const queryClient = useQueryClient()
const prefetch = (postId) => {
queryClient.prefetchQuery({
queryKey: ["post", postId],
queryFn: () => fetchPost(postId),
staleTime: 60000
})
}
return (
<ul>
{posts.map(post => (
<li key={post.id} onMouseEnter={() => prefetch(post.id)}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
Key Takeaways
- Automatic caching with configurable stale times
- Background refetching keeps data fresh
- Optimistic updates for instant UI feedback
- Infinite queries for pagination/infinite scroll
- Prefetching on hover for instant navigation
- Framework-agnostic — works with React, Vue, Solid, Svelte, Angular
Explore TanStack Query docs for the complete API.
Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.
Top comments (0)