This episode teaches the first correct setup steps: creating a QueryClient with default options, wrapping the application with QueryClientProvider, and configuring global settings such as staleTime, retry, refetchOnWindowFocus, and gcTime.

In episode 2 you already had the architectural mental model. Now it's time to write real code. The first correct step determines how comfortable all the following episodes feel: setting up QueryClient as the cache center, wrapping the application with QueryClientProvider, and configuring global settings so the behavior of all queries is consistent from the start.
Episode 3 covers all three practically. By the end of the episode, your application will be ready to accept the useQuery hook in episode 4.
QueryClient is the container for the cache and configuration. It is usually created once at the application level and stored in a module variable:
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()Creating the instance outside a component means the cache is shared across the entire application and isn't recreated on every render. new QueryClient() above produces an instance with the library's built-in default configuration.
You can provide default options directly at creation time. This is a common pattern used in real applications:
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 2,
refetchOnWindowFocus: true,
gcTime: 10 * 60 * 1000,
},
},
})The defaultOptions.queries.staleTime configuration is applied to every query that doesn't set its own staleTime. staleTime: 5 * 60 * 1000 means data is considered fresh for 5 minutes.
So that all hooks can access the QueryClient, wrap the application with QueryClientProvider. In a Vite project, this is done in src/main.tsx:
import { QueryClientProvider } from "@tanstack/react-query"
import { queryClient } from "./query-client"
import { App } from "./App"
function Main() {
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
}The provider accepts a client prop containing the QueryClient instance. QueryClientProvider client={queryClient} makes the cache accessible from any component inside the tree.
A simple rule: create one QueryClient and one QueryClientProvider at the application root. Creating the instance inside a component will create a new cache on every render — that's a common bug and often hard to find.
Warning
Don't call new QueryClient() inside a component or inside a render function. The cache will be reset over and over, and all of TanStack Query's caching features will be dead. Keep the instance at module scope as in the example above.
Sets how long data is considered fresh. A value of 0 (the default) makes data stale immediately every time a component mounts. For applications whose data rarely changes, raise it to 30 seconds, 5 minutes, or longer. The trade-off: a large staleTime reduces the number of requests, but data can fall behind the server.
How many times a failed query is retried. TanStack Query's default is 3 times with an increasing delay. For important read operations, keep the default; for queries that often fail because of user input, you can lower it to 1.
When the user returns to the browser tab, TanStack Query refetches stale queries by default. This is a very useful feature — data is always fresh when the user returns. Set it to false if your application needs full control.
How long the cache survives in memory after there are no observers. The v5 default is 5 minutes (5 * 60 * 1000). A larger value makes data last longer when navigating between pages, at the cost of memory usage.
staleTime → 0 seconds
retry → 3 times
refetchOnWindowFocus → true
gcTime → 5 minutes
structuralSharing → trueThe summary staleTime → 0 seconds shows the built-in v5 defaults. Remember these numbers because we will often refer to them as a baseline in the episodes that follow.
With the provider mounted and the global configuration set, your application's final structure becomes:
// src/query-client.ts
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 2,
refetchOnWindowFocus: true,
gcTime: 10 * 60 * 1000,
},
},
})One file for the instance and configuration, one provider at the root, and the entire application is ready to use a consistent cache. Splitting files in this way is also the seed of the folder architecture we will discuss in episode 21.
Episode 3 completed the installation foundation: creating QueryClient with default options at module scope, wrapping the application with QueryClientProvider, and understanding global configuration such as staleTime, retry, refetchOnWindowFocus, and gcTime.
Key takeaways:
QueryClient once at module scope, not inside a component.QueryClientProvider.defaultOptions.queries becomes the baseline configuration for all queries.staleTime 0, retry 3, gcTime 5 minutes.staleTime and gcTime are two different things and must be set deliberately.QueryClient, one provider.In the next episode, episode 4, we will discuss basic useQuery — creating your first query with queryKey and queryFn, reading the data, isLoading, isError, and isFetching statuses, and the correct query function pattern. This is where you truly feel the difference from the manual useEffect pattern.