🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

TanStack Query Introduction: Caching and Sync, Solved

An introduction to TanStack Query: QueryClientProvider setup, the useQuery hook, and automatic background refetching.

Total XP: 0|💻 react XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

TanStack Query fundamentals.

Quick Quiz //

What does a shared queryKey enable across two different components?


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

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

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

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

THE BUG

Two components fetching what should be the same data show inconsistent, unsynced values.

THE FIX

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.

THE BUG

Data refetches on every single render, causing excessive network requests.

THE FIX

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);

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Creating `new QueryClient()` inside a component's render body

// Wrong function App() { const queryClient = new QueryClient(); // recreated every render } // Correct const queryClient = new QueryClient(); // created once, at module scope function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider>; }

The Solution //

This recreates the entire cache on every render, defeating caching entirely and causing excessive refetching. Instantiate the QueryClient once, outside the component or via a stable useState initializer.

The Error //

Using an inconsistent queryKey shape for logically identical data across the codebase

// Centralize the queryKey to guarantee consistency function useUser(userId) { return useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) }); }

The Solution //

If one component uses ['user', userId] and another uses ['users', userId] for the same conceptual data, they'll never share a cache entry. Standardize queryKey conventions across the codebase, often by centralizing them in a shared hook.

Lesson Glossary

[01]QueryClient

The object holding TanStack Query's entire shared cache for an application.

Code Preview
new QueryClient()

[02]QueryClientProvider

The context provider component that makes a QueryClient's cache available throughout the app.

Code Preview
<QueryClientProvider client={queryClient}>

[03]useQuery

The hook for fetching and caching data, keyed by a queryKey and populated by a queryFn.

Code Preview
useQuery({ queryKey, queryFn })

[04]queryKey

A unique array identifying a cached query, used to share and invalidate cache entries.

Code Preview
['user', userId]

[05]Background Refetching

TanStack Query's default behavior of refetching data on window focus, reconnect, or remount.

Code Preview
refetchOnWindowFocus (default: true)

Continue Learning