A hand-rolled useFetch hook re-fetches from scratch in every component, with no shared cache and no automatic freshness logic. TanStack Query is the industry-standard library that solves all of this. This lesson covers QueryClientProvider, useQuery, and automatic background refetching.
1The Problems a Custom useFetch Doesn't Solve
A hand-rolled useFetch hook works for a single component but doesn't share a cache across components, doesn't deduplicate identical in-flight requests, and doesn't refetch stale data automatically. TanStack Query is the industry-standard library built specifically to solve these problems.
2Setting Up the QueryClientProvider
TanStack Query requires a single QueryClient instance, created once and provided to the app through a QueryClientProvider wrapping the component tree. This client holds the entire shared cache that every useQuery call throughout the app reads from and writes to.
3The useQuery Hook
useQuery accepts a unique queryKey used for cache lookup and a queryFn that returns a promise resolving the data. It returns data, isLoading, isError, and more — critically, calling it with the same key from multiple components shares the exact same cached data and in-flight request.
4Automatic Background Refetching
By default, TanStack Query automatically refetches data when the browser window regains focus, the network reconnects, or a component using the query re-mounts, keeping displayed data fresh without any manually written refetch logic.
5Loading, Error, and Success States — Built In
Every useQuery call returns consistent isLoading, isError, and isSuccess flags, eliminating the need to hand-roll separate state variables for every fetch. This consistency makes loading and error handling predictable across an entire codebase.
6Step-by-Step Breakdown
The Problems a Custom useFetch Doesn't Solve. Your own useFetch hook works, but every component that calls it re-fetches from scratch, has no shared cache, and doesn't refetch stale data when the user switches back to the tab. TanStack Query (formerly React Query) is the industry-standard library that solves all of this out of the box.
Setting Up the QueryClientProvider. TanStack Query needs a single QueryClient instance wrapping your app in a QueryClientProvider. This client holds the entire cache — every query anywhere in your app reads from and writes to this one shared store.
Why does TanStack Query require a single QueryClientProvider wrapping the app?
- →It holds the shared cache every useQuery call in the app reads and writes to
- →It's purely required JSX boilerplate with no functional purpose
The useQuery Hook. useQuery takes a unique queryKey (used for caching) and a queryFn that returns a promise. It returns data, isLoading, isError, and more — but unlike your custom hook, calling useQuery with the same key from two different components shares the exact same cached data and in-flight request.
Automatic Background Refetching. By default, TanStack Query automatically refetches data when the browser window regains focus, when the network reconnects, or when a mounted component re-mounts — keeping displayed data fresh without you writing a single line of that logic yourself. This is one of the biggest wins over a hand-rolled fetch hook.
By default, what does TanStack Query do when a user switches back to a browser tab showing already-fetched data?
- →It automatically refetches the data in the background to keep it fresh
- →Nothing happens — data only ever refetches on a manual page reload
Loading, Error, and Success States — Built In. Every useQuery call returns consistent isLoading, isError, and isSuccess flags, so you don't hand-roll three separate useState calls per fetch like you did with a custom hook. This consistency across the entire codebase makes loading and error UI predictable everywhere.
Mastery Achieved. You now understand the fundamentals of TanStack Query: a shared QueryClient cache via QueryClientProvider, the useQuery hook with queryKey and queryFn, automatic background refetching, and consistent loading/error states across your whole app. Next, you'll go deeper into how that caching layer actually works.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Consistent Query States Simplify Accessible Loading/Error UI
Because every useQuery call exposes the same isLoading/isError shape, it's easy to build one reusable, accessible loading and error component (with proper aria-busy and error announcements) and reuse it consistently across every data-driven screen.
SEO Implications
- 1
TanStack Query Is a Client-Side Caching Layer
It manages client-side data fetching and caching after hydration; for content that needs to be present in initial server-rendered HTML for SEO, data should still be fetched server-side (e.g. in a Server Component) rather than relying solely on client-side useQuery.
Best Practices
Design queryKeys to Reflect Their Actual Dependencies
Include every value the query function depends on in the queryKey array (like ['user', userId]) so the cache correctly treats different parameter combinations as separate cached entries.
Create the QueryClient Once, Outside of Component Render
Instantiate new QueryClient() at module scope (or inside a ref/useState initializer), never directly inside a component's render body, to avoid recreating the entire cache on every render.
Frequent Bugs
Two components fetching what should be the same data show inconsistent, unsynced values.
They're likely using different queryKeys for conceptually the same data, so each gets its own separate cache entry instead of sharing one. Standardize the queryKey shape for identical logical queries.
Data refetches on every single render, causing excessive network requests.
The QueryClient is probably being recreated inside the component's render function instead of being instantiated once outside of it, effectively resetting the cache constantly. Move `new QueryClient()` outside the component or into a stable ref.
Real-World Examples
Sharing a User Profile Query Across Components
Both a page header showing the user's avatar and a settings page showing their full profile need the same user data. Using useQuery({ queryKey: ['user', userId], queryFn: fetchUser }) in both components means only one network request is made, and both components stay in sync automatically when the cache updates.
function useUser(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
}
// Used in both Header and SettingsPage — shares one cached request
const { data: user } = useUser(currentUserId);