FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

TanStack Query (React Query): Server State Management Done Right

Master TanStack Query for server state management. Covers useQuery, useMutation, query invalidation, optimistic updates, infinite queries, prefetching, and how it compares to Redux.

Yusuf SeyitoğluMarch 17, 20263 views10 min read

TanStack Query (React Query): Server State Management Done Right

Most React applications mix two fundamentally different kinds of state: client state (UI toggles, form inputs, selected items) and server state (data fetched from an API). Redux and useState handle client state well, but server state has unique challenges β€” caching, background refetching, stale data, loading states β€” that general state libraries handle poorly. TanStack Query (formerly React Query) solves these problems elegantly.

The Problem with Manual Data Fetching

Without TanStack Query, fetching data looks like this:

javascript
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (!cancelled) { setUser(data); setLoading(false); } }) .catch(err => { if (!cancelled) { setError(err.message); setLoading(false); } }); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error message={error} />; return <div>{user.name}</div>; }

Every component repeats this pattern. There is no caching β€” navigating away and back re-fetches. There is no background refresh. Multiple components fetching the same resource make duplicate requests.

Setup

bash
npm install @tanstack/react-query @tanstack/react-query-devtools
jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, -- data fresh for 5 minutes retry: 2, -- retry failed requests twice refetchOnWindowFocus: true, -- refetch when tab regains focus }, }, }); function App() { return ( <QueryClientProvider client={queryClient}> <Router /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }

useQuery: Fetching Data

javascript
import { useQuery } from "@tanstack/react-query"; async function fetchUser(userId) { const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error("Failed to fetch user"); return response.json(); } function UserProfile({ userId }) { const { data: user, isLoading, isError, error, isFetching, -- true even on background refetches isStale, -- true when data is older than staleTime refetch, -- manually trigger a refetch } = useQuery({ queryKey: ["users", userId], -- unique cache key queryFn: () => fetchUser(userId), staleTime: 60_000, -- override default: fresh for 1 minute enabled: !!userId, -- only run when userId is truthy }); if (isLoading) return <Spinner />; if (isError) return <Error message={error.message} />; return ( <div> {isFetching && <small>Refreshing...</small>} <h1>{user.name}</h1> </div> ); }

The query key is the cache key. ["users", 1] and ["users", 2] are separate cache entries. When userId changes, TanStack Query automatically fetches the new data.

useQuery with Dependent Queries

javascript
function UserPosts({ userId }) { -- First fetch the user const { data: user } = useQuery({ queryKey: ["users", userId], queryFn: () => fetchUser(userId), }); -- Then fetch their posts (only when user is available) const { data: posts } = useQuery({ queryKey: ["posts", { authorId: user?.id }], queryFn: () => fetchPosts({ authorId: user.id }), enabled: !!user, -- waits for user to be loaded }); return <PostList posts={posts} />; }

Parallel Queries

javascript
function Dashboard({ userId }) { -- Both queries run simultaneously const userQuery = useQuery({ queryKey: ["users", userId], queryFn: () => fetchUser(userId) }); const postsQuery = useQuery({ queryKey: ["posts", userId], queryFn: () => fetchUserPosts(userId) }); const statsQuery = useQuery({ queryKey: ["stats", userId], queryFn: () => fetchStats(userId) }); if (userQuery.isLoading || postsQuery.isLoading) return <Spinner />; return ( <div> <UserCard user={userQuery.data} /> <PostList posts={postsQuery.data} /> {statsQuery.data && <StatsChart data={statsQuery.data} />} </div> ); }

useMutation: Creating, Updating, Deleting

javascript
import { useMutation, useQueryClient } from "@tanstack/react-query"; function CreatePostForm() { const queryClient = useQueryClient(); const createPost = useMutation({ mutationFn: async (newPost) => { const response = await fetch("/api/posts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newPost), }); if (!response.ok) throw new Error("Failed to create post"); return response.json(); }, -- Invalidate the posts list so it refetches onSuccess: (createdPost) => { queryClient.invalidateQueries({ queryKey: ["posts"] }); -- Or add the new post directly to cache queryClient.setQueryData(["posts", createdPost.id], createdPost); }, onError: (error) => { toast.error(`Failed to create post: ${error.message}`); }, }); return ( <form onSubmit={e => { e.preventDefault(); createPost.mutate({ title: e.target.title.value, content: e.target.content.value }); }}> <input name="title" /> <textarea name="content" /> <button type="submit" disabled={createPost.isPending}> {createPost.isPending ? "Creating..." : "Create Post"} </button> {createPost.isError && <p className="error">{createPost.error.message}</p>} </form> ); }

Optimistic Updates

Update the UI immediately before the server confirms, then reconcile:

javascript
const toggleLike = useMutation({ mutationFn: ({ postId, liked }) => fetch(`/api/posts/${postId}/like`, { method: liked ? "POST" : "DELETE", }), onMutate: async ({ postId, liked }) => { -- Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ["posts", postId] }); -- Snapshot the previous value const previousPost = queryClient.getQueryData(["posts", postId]); -- Optimistically update the cache queryClient.setQueryData(["posts", postId], old => ({ ...old, liked, likeCount: liked ? old.likeCount + 1 : old.likeCount - 1, })); -- Return context with snapshot for rollback return { previousPost }; }, onError: (err, variables, context) => { -- Rollback on error queryClient.setQueryData(["posts", variables.postId], context.previousPost); }, onSettled: (data, error, variables) => { -- Always refetch to sync with server queryClient.invalidateQueries({ queryKey: ["posts", variables.postId] }); }, });

Infinite Queries (Pagination / Load More)

javascript
import { useInfiniteQuery } from "@tanstack/react-query"; function PostFeed() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ["posts"], queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}&limit=20`).then(r => r.json()), getNextPageParam: (lastPage, allPages) => { return lastPage.hasMore ? allPages.length + 1 : undefined; }, initialPageParam: 1, }); const posts = data?.pages.flatMap(page => page.posts) ?? []; return ( <div> {posts.map(post => <PostCard key={post.id} post={post} />)} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage} > {isFetchingNextPage ? "Loading..." : "Load More"} </button> )} </div> ); }

Prefetching

Prefetch data before the user navigates to improve perceived performance:

javascript
const queryClient = useQueryClient(); -- Prefetch on hover (before user clicks) function PostLink({ post }) { return ( <Link to={`/posts/${post.id}`} onMouseEnter={() => { queryClient.prefetchQuery({ queryKey: ["posts", post.id], queryFn: () => fetchPost(post.id), staleTime: 10_000, }); }} > {post.title} </Link> ); } -- Prefetch on the server (Next.js) export async function getServerSideProps() { const queryClient = new QueryClient(); await queryClient.prefetchQuery({ queryKey: ["posts"], queryFn: fetchPosts, }); return { props: { dehydratedState: dehydrate(queryClient) }, }; }

Common Interview Questions

Q: What is the difference between staleTime and cacheTime (gcTime)?

staleTime controls how long data is considered fresh β€” during this period, no background refetch occurs and the cached data is returned immediately. After staleTime, data is considered stale and will be refetched in the background on the next mount or focus. gcTime (formerly cacheTime) controls how long inactive query data stays in memory before being garbage collected. Default: staleTime=0 (always stale), gcTime=5 minutes.

Q: How does query invalidation work?

queryClient.invalidateQueries marks matching queries as stale and triggers a background refetch if they are currently mounted. You pass a query key filter β€” { queryKey: ["posts"] } invalidates all queries whose key starts with "posts". This is the standard pattern after a mutation: invalidate related queries so they automatically refresh.

Q: When would you use TanStack Query instead of Redux for server data?

TanStack Query is purpose-built for server state β€” async fetching, caching, deduplication, background refresh, and pagination. Redux requires significant boilerplate (actions, reducers, thunks) to handle these patterns. Use TanStack Query for all API data; use Redux or Zustand for pure client state that does not come from a server. Many teams use both: TanStack Query for data fetching, Zustand for UI state.

Practice on Froquiz

Data fetching patterns and state management are key React interview topics. Test your React knowledge on Froquiz β€” covering hooks, rendering, performance, and patterns.

Summary

  • TanStack Query manages server state β€” caching, deduplication, background refresh, loading states
  • queryKey is the cache key β€” array format enables precise cache control
  • staleTime controls freshness; gcTime controls memory retention
  • useMutation handles writes; onSuccess invalidates related queries to trigger refetch
  • Optimistic updates: update cache immediately, roll back on error, always refetch on settle
  • useInfiniteQuery handles paginated and "load more" patterns cleanly
  • Prefetch on hover for instant navigation β€” data is ready before the user clicks

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • Python Testing with pytest: Fixtures, Parametrize, Mocking and Best PracticesMar 17
  • PostgreSQL Full-Text Search: tsvector, tsquery, Ranking and Multilingual SearchMar 17
  • Software Architecture Patterns: MVC, Clean Architecture, Hexagonal and Event-DrivenMar 17
All Blogs