| | |

TanStack Query 01 ⚛️ Installation — Package setup & query client provider

⏱️ Estimated read: 12 min | |

TanStack Query does not replace fetch or axios. It sits above them, managing the lifecycle of asynchronous server state: caching responses, deduplicating concurrent requests, marking data stale, and refetching in the background. Before any of that can happen, two pieces must be in place: the package itself, and a QueryClient instance made available to every component through React context. This chapter covers the installation of @tanstack/react-query, the creation of the QueryClient, and the wrapping of the application in QueryClientProvider. It also covers the optional DevTools package and the v5-specific requirements that catch developers migrating from v4.

Key point: The QueryClient is the cache and the scheduler. It must be created once, outside the component tree or inside a stable module scope, and passed to QueryClientProvider at the root of the application. Every useQuery call below that provider reads from and writes to that single client.


Why installation setup matters

The provider requirement problem.

TanStack Query does not work through global variables or module-level singletons. It uses React context to distribute the QueryClient to the components that call useQuery, useMutation, and useQueryClient. If the provider is missing, the hooks throw an error: “No QueryClient set, use QueryClientProvider to set one.” The provider is not optional ceremony. It is the mechanism that connects every query hook to the cache .

The v5 syntax break problem.

Version 5 removed the function overloads that v4 accepted. In v4, useQuery(['todos'], fetchTodos, options) worked. In v5, only object syntax is valid: useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) . This is not a stylistic preference. The library’s TypeScript types no longer include the array-and-function signature. A codebase installed with v5 but written in v4 syntax will fail to compile. The installation chapter must state this explicitly because the first error a migrating developer encounters is usually a type error on a query call that looks correct in old documentation.

The React version floor problem.

TanStack Query v5 requires React 18 or later . React 17 applications cannot use v5 without upgrading React first. The v4 line remains available for applications pinned to older React versions, but the installation command must specify @4 to receive it. Installing @tanstack/react-query without a version tag installs the latest v5 release and fails silently against React 17’s peer dependency expectations.


a. Installing the package

The core package is published as @tanstack/react-query. It works with ReactDOM and React Native, and it requires React 18 or higher .

# Install the core package with npm
npm install @tanstack/react-query

# Or with pnpm
pnpm add @tanstack/react-query

# Or with yarn
yarn add @tanstack/react-query

# Or with bun
bun add @tanstack/react-query

The package has no peer dependency on a specific data-fetching library. It does not care whether your queryFn calls fetch, axios, graphql-request, or a custom SDK. The installation is complete once the package is in node_modules and the import resolves.

The official documentation also recommends the ESLint plugin, which catches common mistakes such as missing query keys or incorrect dependency arrays in query options .

# Install the ESLint plugin as a development dependency
npm install -D @tanstack/eslint-plugin-query

The plugin is not required for the library to function. It is a quality tool that flags patterns the TypeScript compiler cannot detect, such as a queryFn that closes over a value not present in the queryKey.


b. Creating the QueryClient and wrapping the application

The QueryClient is the central object. It holds the cache, the default options, and the scheduling logic. It should be created once per application, outside the React component tree, so that it is not recreated on every render.

// src/main.tsx or src/App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Create the client once, outside the component
// Default options apply to every query unless overridden locally
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,   // Data is fresh for 5 minutes
      gcTime: 1000 * 60 * 60,     // Unused cache entries are garbage-collected after 1 hour
      refetchOnWindowFocus: false, // Do not refetch when the browser tab regains focus
    },
  },
})

function App() {
  return (
    // The provider makes the client available to every descendant component
    <QueryClientProvider client={queryClient}>
      <YourApplication />
    </QueryClientProvider>
  )
}

The staleTime default controls how long data is considered fresh. While data is fresh, useQuery returns the cached value without refetching. After staleTime elapses, the data becomes stale, and the next mount or window focus triggers a background refetch . The gcTime default controls how long stale, unused cache entries remain in memory before being discarded. In v4, this option was named cacheTime. In v5, the name changed to gcTime to reflect its actual function: garbage collection time .

The provider must wrap the part of the tree that contains query hooks. In a typical React application, this is the root component or the file that calls createRoot. If the application uses Next.js or TanStack Start, the provider goes in the client-side layout, and server-side prefetching uses a separate hydration mechanism.


c. Optional DevTools and framework-specific packages

The DevTools package provides a visual panel that shows the state of every query: fresh, fetching, stale, or inactive. It is a development-only tool and should not be shipped in production bundles.

# Install DevTools as a development dependency
npm install -D @tanstack/react-query-devtools
// Add the DevTools component inside the provider
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApplication />
      {/* DevTools renders only in development by default */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

For projects using multiple TanStack libraries—Query and Router, for example—the unified @tanstack/react-devtools package provides a single shell that hosts multiple panels. The individual DevTools components are still available as panels for that shell .

TanStack Query also ships framework-specific packages. The React package is @tanstack/react-query. Vue uses @tanstack/vue-query, Svelte uses @tanstack/svelte-query, and Angular uses @tanstack/angular-query-experimental . The installation and provider setup differ per framework. This chapter covers the React package exclusively.


Complete Example Session

// ============================================
// PART 1: THE IMPORT STATEMENTS
// ============================================
// QueryClient and QueryClientProvider are the two required imports
// ReactQueryDevtools is optional and development-only
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

// ============================================
// PART 2: CREATING THE QUERY CLIENT
// ============================================
// The client is created once at module scope
// It is NOT created inside a component or a render function
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Fresh data is not refetched on mount or focus
      staleTime: 1000 * 60 * 5,
      // Stale data is kept in memory for one hour after last use
      gcTime: 1000 * 60 * 60,
      // Disable automatic refetch on window focus for a quieter default
      refetchOnWindowFocus: false,
      // Retry failed requests once before surfacing the error
      retry: 1,
    },
  },
})

// ============================================
// PART 3: THE PROVIDER WRAPPER
// ============================================
// The provider receives the client as a prop
// Every component below it can call useQuery, useMutation, useQueryClient
function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

// ============================================
// PART 4: THE APPLICATION ROOT
// ============================================
// The providers wrap the entire application
// In a Vite app, this component is rendered by createRoot in main.tsx
function App() {
  return (
    <AppProviders>
      <TodoList />
    </AppProviders>
  )
}

// ============================================
// PART 5: A COMPONENT THAT USES THE CLIENT
// ============================================
// This component does not receive the client as a prop
// It reads it from context via the useQuery hook
import { useQuery } from '@tanstack/react-query'

function TodoList() {
  const { data, isPending, error } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const response = await fetch('/api/todos')
      if (!response.ok) throw new Error('Failed to fetch todos')
      return response.json()
    },
  })

  if (isPending) return <p>Loading todos...</p>
  if (error) return <p>Error: {error.message}</p>

  return (
    <ul>
      {data.map((todo: { id: number; title: string }) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

// ============================================
// PART 6: THE ENTRY POINT (main.tsx)
// ============================================
// This is the file that connects React to the DOM
// The App component contains the QueryClientProvider
import { createRoot } from 'react-dom/client'
import App from './App'

const root = createRoot(document.getElementById('root')!)
root.render(<App />)

The six parts demonstrate the complete installation sequence: import, create the client with defaults, wrap the tree in the provider, render a consumer component, and mount the application. The TodoList component never sees the QueryClient directly. It calls useQuery and receives data, status, and error from the context that the provider established.


Quick Reference

Installation Commands

PackageCommandPurpose
Corenpm install @tanstack/react-queryRequired
DevToolsnpm install -D @tanstack/react-query-devtoolsOptional, development
ESLint pluginnpm install -D @tanstack/eslint-plugin-queryOptional, quality

Required Provider Setup

StepCodeLocation
Create clientconst queryClient = new QueryClient({...})Module scope
Import providerimport { QueryClientProvider } from '@tanstack/react-query'Root file
Wrap tree<QueryClientProvider client={queryClient}>...</QueryClientProvider>Root component
Verify hooks workuseQuery({ queryKey, queryFn })Any descendant

v5 Requirements

RequirementValueNote
React version18.0+v4 supports React 16.8+
Hook syntaxObject onlyuseQuery({ queryKey, queryFn })
cacheTimeRenamed to gcTimev4 name removed
Query callbacksRemoved from queriesonSuccess/onError still work in mutations

Best Practices

✅ Do This:

// Create the client once at module scope
const queryClient = new QueryClient({ defaultOptions: {...} })  // ✅

// Set explicit defaults for staleTime and gcTime
staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 60                 // ✅

// Wrap the root component with QueryClientProvider
<QueryClientProvider client={queryClient}>...</QueryClientProvider> // ✅

// Use object syntax for useQuery
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })           // ✅

// Use isPending for initial loading state
if (isPending) return <Spinner />                                // ✅

// Install DevTools only in development
npm install -D @tanstack/react-query-devtools                    // ✅

❌ Don’t Do This:

// Create the client inside a component (recreated on every render)
function App() { const queryClient = new QueryClient() }          // ❌

// Omit the provider and call useQuery directly
function Component() { useQuery({...}) }                          // ❌ Throws

// Use v4 array syntax with v5
useQuery(['todos'], fetchTodos)                                   // ❌ Type error

// Use cacheTime instead of gcTime
cacheTime: 1000 * 60 * 60                                         // ❌ Deprecated

// Use isLoading as "no data yet" in v5
if (isLoading) return <Spinner /> // Now means pending AND fetching // ❌

// Ship DevTools in production bundle
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // ❌

Common Pitfalls

PitfallWhy It HappensFix
“No QueryClient set” errorQueryClientProvider missing from treeWrap root component
Type error on useQueryv5 installed but v4 syntax usedUse object syntax
Client recreated on every rendernew QueryClient() inside componentMove to module scope
cacheTime not recognizedv4 option name used in v5Rename to gcTime
React 17 peer dependency warningv5 requires React 18Upgrade React or install @4
DevTools panel emptyProvider missing or client not passedVerify provider wraps DevTools
Data never refetchesstaleTime set too high or InfinityLower staleTime for volatile data

Real-World Examples

1. Minimal provider setup

const queryClient = new QueryClient()
<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

2. Provider with custom defaults

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 60_000, retry: 2 } },
})

3. Provider with DevTools

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

4. Separate provider file

// providers.tsx
export function Providers({ children }) {
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}

5. Client with no window-focus refetch

const queryClient = new QueryClient({
  defaultOptions: { queries: { refetchOnWindowFocus: false } },
})

6. Verify installation with a query

const { data } = useQuery({ queryKey: ['health'], queryFn: () => fetch('/health').then(r => r.json()) })

7. ESLint plugin setup

npm install -D @tanstack/eslint-plugin-query
# Add "plugin:@tanstack/eslint-plugin-query/recommended" to .eslintrc

8. Check installed version

npm list @tanstack/react-query
# Output: @tanstack/react-query@5.x.x

9. Import types for query options

import type { QueryClientConfig } from '@tanstack/react-query'

10. Confirm provider in tests

// Test wrapper must include QueryClientProvider with a fresh client per test
const testClient = new QueryClient()
render(<QueryClientProvider client={testClient}><Component /></QueryClientProvider>)

Visual

The Provider and Client Relationship

┌──────────────────────────────────────────────────────────────┐
│  QUERYCLIENTPROVIDER DISTRIBUTES THE CACHE                   │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  QueryClient (module scope)                            │  │
│  │  ├─ Cache: queryKey → data                             │  │
│  │  ├─ Default options: staleTime, gcTime, retry          │  │
│  │  └─ Scheduler: refetch, invalidate, background updates │  │
│  └──────────────────────────┬─────────────────────────────┘  │
│                             │                                │
│                    passed as prop                            │
│                             │                                │
│  ┌──────────────────────────▼─────────────────────────────┐  │
│  │  <QueryClientProvider client={queryClient}>            │  │
│  │                                                        │  │
│  │    ┌──────────────┐    ┌──────────────┐                │  │
│  │    │ Component A  │    │ Component B  │                │  │
│  │    │ useQuery({}) │    │ useQuery({}) │                │  │
│  │    └──────┬───────┘    └──────┬───────┘                │  │
│  │           │                   │                        │  │
│  │           └─────────┬─────────┘                        │  │
│  │                     │ reads/writes cache               │  │
│  │                     ▼                                  │  │
│  │              same QueryClient                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Without the provider, useQuery throws:                      │
│  "No QueryClient set, use QueryClientProvider to set one"    │
└──────────────────────────────────────────────────────────────┘

Installation Decision Flow

┌──────────────────────────────────────────────────────────────┐
│  WHICH PACKAGE AND VERSION?                                  │
│                                                              │
│  Is your app on React 18 or later?                           │
│       │                                                      │
│       ├── YES → Install @tanstack/react-query (v5)           │
│       │         Use object syntax:                           │
│       │         useQuery({ queryKey, queryFn })              │
│       │                                                      │
│       └── NO  → Upgrade React first, OR                      │
│                 install @tanstack/react-query@4              │
│                 (v4 supports React 16.8+)                    │
│                                                              │
│  Need a visual cache inspector during development?           │
│       └── YES → npm install -D @tanstack/react-query-devtools│
│                                                              │
│  Want lint rules for query patterns?                         │
│       └── YES → npm install -D @tanstack/eslint-plugin-query │
└──────────────────────────────────────────────────────────────┘

v4 to v5 Hook Syntax

┌──────────────────────────────────────────────────────────────┐
│  SYNTAX CHANGE THAT BREAKS COMPILATION                       │
│                                                              │
│  v4 (accepted):                                              │
│  useQuery(['todos'], fetchTodos, { staleTime: 5000 })        │
│       │        │           │                                 │
│       │        │           └── options object                │
│       │        └── query function                            │
│       └── query key array                                    │
│                                                              │
│  v5 (required):                                              │
│  useQuery({                                                  │
│    queryKey: ['todos'],       ← key inside object            │
│    queryFn: fetchTodos,       ← function inside object       │
│    staleTime: 5000,           ← options alongside            │
│  })                                                          │
│                                                              │
│  The array-and-function overloads were removed in v5.        │
│  Installing v5 with v4 syntax produces type errors.          │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Core package@tanstack/react-query
React requirement18.0 or later for v5
Required importsQueryClient, QueryClientProvider
Client creationnew QueryClient({ defaultOptions })
Client locationModule scope, outside components
Provider component<QueryClientProvider client={queryClient}>
Provider placementRoot of the component tree
DevTools package@tanstack/react-query-devtools (development only)
v5 hook syntaxObject only: useQuery({ queryKey, queryFn })
Renamed optioncacheTime → gcTime in v5
Loading stateisPending for “no data yet” in v5

Key takeaways:

  • The provider is mandatory, not decorative. Without QueryClientProvider, every query hook throws. The provider is the bridge between the module-scope client and the component tree.
  • Create the client once. A QueryClient created inside a component is recreated on every render, discarding the cache and the scheduler state. Module scope is the correct location.
  • v5 requires object syntax. The array-and-function overloads from v4 were removed. useQuery({ queryKey, queryFn }) is the only valid form .
  • React 18 is the floor for v5. Applications on React 17 must either upgrade React or pin the query package to @4 .
  • gcTime replaced cacheTime. The option name changed in v5 to reflect its function: how long unused cache entries are retained before garbage collection .
  • DevTools is development-only. Install it as a dev dependency and render it inside the provider. It should not appear in production bundles.
  • Defaults belong on the client. Set staleTime, gcTime, and retry in defaultOptions on the QueryClient so that every query inherits sensible behavior without repeating configuration.

Remember: Installation is two lines of code and one provider wrapper, but every subsequent chapter depends on this foundation being correct. The QueryClient holds the cache. The provider distributes it. The hooks consume it. If the client is created in the wrong place, the cache is lost on every render. If the provider is missing, the hooks throw. If the version is wrong, the syntax fails. Get the installation right—React 18, object syntax, module-scope client, root-level provider—and the rest of TanStack Query behaves as designed.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!