DEV Community

Munna Thakur
Munna Thakur

Posted on

⚡ Mastering useQuery — The Complete Dev Guide (TanStack React Query)

The biggest challenge in modern React apps:

  • ✅ Data fetching
  • ✅ Caching
  • ✅ Background sync
  • ✅ Retry logic
  • ✅ Loading/error state handling
  • ✅ Avoiding unnecessary re-renders

@tanstack/react-query's useQuery provides a production-grade solution for all of these.

This article is for developers from beginner to advanced — especially if you want to understand performance and internals.


📋 Table of Contents


⚡ Quick Summary

TL;DR: useQuery is your production-ready solution for:

  • ✅ Automatic caching & background sync
  • ✅ Loading/error states out of the box
  • ✅ Smart refetch strategies (window focus, network reconnect)
  • ✅ Retry logic with exponential backoff
  • ✅ Optimized re-renders (structural sharing)
  • ✅ Zero boilerplate for common patterns

Article Length: ~20 min read | Skill Level: Beginner to Advanced


📦 Installation & Setup

# npm
npm install @tanstack/react-query

# yarn
yarn add @tanstack/react-query

# pnpm
pnpm add @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Basic Setup:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

🔧 Pro Tip: Install React Query DevTools for debugging:

npm install @tanstack/react-query-devtools

🔗 GitHub Repository:

https://github.com/TanStack/query


🧠 Why useQuery? {#why-usequery}

Traditional Way:

useEffect(() => {
  setLoading(true)
  fetch('/api')
    .then(res => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false))
}, [])
Enter fullscreen mode Exit fullscreen mode

❌ Problems:

  • No caching
  • No retry
  • No background refetch
  • No stale handling
  • Manual loading state
  • Hard to scale

useQuery makes server state lifecycle-aware.

💡 Key Insight: React Query isn't just a data fetching library - it's a complete async state manager specifically designed for server state.


🚀 Basic Usage {#basic-usage}

import { useQuery } from '@tanstack/react-query'

function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  if (isLoading) return <Spinner />
  if (error) return <Error message={error.message} />

  return <UserList data={data} />
}
Enter fullscreen mode Exit fullscreen mode

🔑 queryKey

  • Unique identity for the query
  • Cache is stored based on this key
  • Key change triggers a refetch
  • Must be serializable and stable
// ✅ Good - Stable keys
['users']
['user', userId]
['posts', { status: 'published', page: 1 }]

// ❌ Bad - Unstable (new reference every render)
[{ id: userId }]
Enter fullscreen mode Exit fullscreen mode

📦 Complete API Reference {#complete-api-reference}

🎯 All Options (Configuration)

const result = useQuery(
  {
    queryKey,                      // Required
    queryFn,                       // Required
    gcTime,
    enabled,
    networkMode,
    initialData,
    initialDataUpdatedAt,
    meta,
    notifyOnChangeProps,
    placeholderData,
    queryKeyHashFn,
    refetchInterval,
    refetchIntervalInBackground,
    refetchOnMount,
    refetchOnReconnect,
    refetchOnWindowFocus,
    retry,
    retryOnMount,
    retryDelay,
    select,
    staleTime,
    structuralSharing,
    subscribed,
    throwOnError,
  },
  queryClient,
)
Enter fullscreen mode Exit fullscreen mode

🎁 All Return Values

const {
  data,                    // Query result data
  dataUpdatedAt,          // Last update timestamp
  error,                  // Error object
  errorUpdatedAt,         // Last error timestamp
  failureCount,           // Failed attempts count
  failureReason,          // Last failure reason
  fetchStatus,            // 'fetching' | 'paused' | 'idle'
  isError,                // Error state boolean
  isFetched,              // At least one fetch completed
  isFetchedAfterMount,    // Fetched after component mount
  isFetching,             // Currently fetching (incl. bg)
  isInitialLoading,       // First fetch in progress
  isLoading,              // Legacy: same as isPending
  isLoadingError,         // Error on initial load
  isPaused,               // Network paused
  isPending,              // No data yet
  isPlaceholderData,      // Showing placeholder
  isRefetchError,         // Error on refetch
  isRefetching,           // Background refetch
  isStale,                // Data is stale
  isSuccess,              // Has successful data
  isEnabled,              // Query is enabled
  promise,                // Promise for data
  refetch,                // Manual refetch function
  status,                 // 'pending' | 'error' | 'success'
} = useQuery(...)
Enter fullscreen mode Exit fullscreen mode

🔵 Core States You Must Understand {#core-states}

1️⃣ isPending (formerly isLoading)

// First fetch is happening, no data available yet
if (isPending) return <Spinner />
Enter fullscreen mode Exit fullscreen mode

2️⃣ isFetching

// Any fetch (including background refetch)
{isFetching && <LoadingIndicator />}
Enter fullscreen mode Exit fullscreen mode

3️⃣ isSuccess

// Data successfully available
if (isSuccess) return <UserList data={data} />
Enter fullscreen mode Exit fullscreen mode

4️⃣ isError

// Query failed
if (isError) return <ErrorMessage error={error} />
Enter fullscreen mode Exit fullscreen mode

🎯 Key Difference:

State isPending isFetching Meaning
Initial Load ✅ true ✅ true First fetch in progress
Background Refetch ❌ false ✅ true Data exists, updating
Success (no fetch) ❌ false ❌ false Data available and fresh
Paused ✅ true ❌ false Network offline
// Pattern for loading states
if (isPending) return <FullPageLoader />  // No data yet
if (isFetching) return <InlineRefreshIcon />  // Updating existing data
Enter fullscreen mode Exit fullscreen mode

🔄 Critical Options Explained {#critical-options-explained}

1. queryKey (Required)

// ✅ Static
queryKey: ['users']

// ✅ Dynamic with params
queryKey: ['users', userId, { status: 'active' }]

// ❌ Unstable (re-creates on every render)
queryKey: [{ id: userId }]  // New object every time!
Enter fullscreen mode Exit fullscreen mode

Rule: QueryKey must be stable and serializable.


2. queryFn (Required)

// Simple
queryFn: () => fetch('/api/users').then(r => r.json())

// With abort signal
queryFn: ({ signal }) => 
  fetch('/api/users', { signal }).then(r => r.json())

// With query key params
queryFn: ({ queryKey }) => {
  const [_key, userId] = queryKey
  return fetchUser(userId)
}
Enter fullscreen mode Exit fullscreen mode

Must return: Promise or throw error.


3. staleTime

// How long data is considered fresh
staleTime: 60 * 1000  // 1 minute

// Fresh data = no refetch on mount/focus
// Stale data = refetch triggered
Enter fullscreen mode Exit fullscreen mode

Default: 0 (immediately stale)

Use cases:

  • Static data: staleTime: Infinity
  • Slow-changing: staleTime: 5 * 60 * 1000
  • Real-time: staleTime: 0

4. gcTime (formerly cacheTime) 🗑

// How long unused query stays in cache
gcTime: 5 * 60 * 1000  // 5 minutes
Enter fullscreen mode Exit fullscreen mode

Default: 5 minutes

Flow:

  1. Component unmount → query becomes inactive
  2. After gcTime → cache cleared
  3. Next mount → fresh fetch

5. enabled 🎚

// Conditional fetching
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  enabled: !!userId  // Only fetch when userId exists
})

// Dependent queries
const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: fetchUserPosts,
  enabled: !!user?.id
})
Enter fullscreen mode Exit fullscreen mode

6. select 🎯

// Data transform (with memoization)
const { data: userNames } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (data) => data.map(user => user.name)
})
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Transforms data
  • Memoized by React Query
  • Component only re-renders when selected data changes

7. refetchInterval 🔁

// Polling
refetchInterval: 5000  // Every 5 seconds

// Conditional polling
refetchInterval: (data) => 
  data?.status === 'processing' ? 2000 : false

// Smart: Stop polling when done
Enter fullscreen mode Exit fullscreen mode

Note: Only active when window is focused (unless refetchIntervalInBackground: true)


8. refetchOnWindowFocus 👁

// Default: true
refetchOnWindowFocus: true

// User switches back to tab → refetch if stale
Enter fullscreen mode Exit fullscreen mode

Best practice:

  • Enable for dashboards
  • Disable for heavy/expensive queries

9. refetchOnMount 🔄

// Default: true
refetchOnMount: true  // Refetch on mount if stale

refetchOnMount: 'always'  // Always refetch
refetchOnMount: false     // Never refetch on mount
Enter fullscreen mode Exit fullscreen mode

10. refetchOnReconnect 📡

// Default: true
// Refetch when network reconnects
refetchOnReconnect: true
Enter fullscreen mode Exit fullscreen mode

11. retry 🔁

// Retry count
retry: 3  // Default

// Custom retry logic
retry: (failureCount, error) => {
  if (error.status === 404) return false
  return failureCount < 3
}
Enter fullscreen mode Exit fullscreen mode

12. retryDelay

// Exponential backoff (default)
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)

// Fixed delay
retryDelay: 1000
Enter fullscreen mode Exit fullscreen mode

13. structuralSharing 🧬

// Default: true
structuralSharing: true

// Deep comparison to preserve references
// Avoids unnecessary re-renders
Enter fullscreen mode Exit fullscreen mode

How it works:

// API returns new object
const newData = { users: [...] }

// React Query checks structure
// If content same → reuses old reference
// React doesn't re-render
Enter fullscreen mode Exit fullscreen mode

Disable when:

  • Huge deeply nested data
  • Performance cost > benefit

14. notifyOnChangeProps 🎯

// Fine-grained render control
notifyOnChangeProps: ['data']

// Component only re-renders when data changes
// Ignores isLoading, isFetching, etc.
Enter fullscreen mode Exit fullscreen mode

Use carefully: Can cause stale UI if misused.


15. placeholderData 📋

// Show temporary data while loading
placeholderData: previousData => previousData

// Or static
placeholderData: []
Enter fullscreen mode Exit fullscreen mode

16. initialData 💾

// Prefilled data (treated as fresh)
initialData: () => getCachedUser()

initialDataUpdatedAt: Date.now()  // When data was fetched
Enter fullscreen mode Exit fullscreen mode

Difference from placeholder:

  • initialData → treated as real data
  • placeholderData → temporary, marked as placeholder

17. networkMode 🌐

// How to handle network status
networkMode: 'online'   // Only fetch when online (default)
networkMode: 'always'   // Fetch regardless
networkMode: 'offlineFirst'  // Try cache first
Enter fullscreen mode Exit fullscreen mode

18. throwOnError ⚠️

// Throw errors instead of returning them
throwOnError: true

// Use with Error Boundaries
Enter fullscreen mode Exit fullscreen mode

19. meta 🏷

// Custom metadata
meta: {
  errorMessage: 'Failed to load users',
  trackingId: 'users-query'
}

// Accessible in queryClient callbacks
Enter fullscreen mode Exit fullscreen mode

🎁 Return Values Explained {#return-values-explained}

Core Data Properties

data

const { data } = useQuery(...)
// Actual query result
// undefined until successful fetch
Enter fullscreen mode Exit fullscreen mode

dataUpdatedAt

const { dataUpdatedAt } = useQuery(...)
// Timestamp (ms) of last successful fetch
// Useful for "Last updated" displays
Enter fullscreen mode Exit fullscreen mode

Error Properties

error

const { error } = useQuery(...)
// Error object from failed queryFn
// null if no error
Enter fullscreen mode Exit fullscreen mode

errorUpdatedAt

const { errorUpdatedAt } = useQuery(...)
// Timestamp of last error
Enter fullscreen mode Exit fullscreen mode

failureCount

const { failureCount } = useQuery(...)
// Number of consecutive failures
// Resets on successful fetch
Enter fullscreen mode Exit fullscreen mode

failureReason

const { failureReason } = useQuery(...)
// Last error that caused failure
Enter fullscreen mode Exit fullscreen mode

Status Booleans

isPending vs isLoading

// isPending: Modern API (v5+)
// isLoading: Legacy (same as isPending)

if (isPending) return <Spinner />
Enter fullscreen mode Exit fullscreen mode

isFetching vs isRefetching

// isFetching: ANY fetch (initial + background)
// isRefetching: Only background fetches

{isFetching && <GlobalLoader />}
{isRefetching && <RefreshIcon spin />}
Enter fullscreen mode Exit fullscreen mode

isInitialLoading

// True only during first fetch
const { isInitialLoading } = useQuery(...)
// Equivalent to: isPending && isFetching
Enter fullscreen mode Exit fullscreen mode

isFetched vs isFetchedAfterMount

// isFetched: At least one successful fetch (lifetime)
// isFetchedAfterMount: Fetched since component mounted

const { isFetched, isFetchedAfterMount } = useQuery(...)
Enter fullscreen mode Exit fullscreen mode

isStale

// Data older than staleTime
const { isStale } = useQuery(...)
// true = eligible for refetch
Enter fullscreen mode Exit fullscreen mode

isPlaceholderData

// Showing placeholder data
const { isPlaceholderData } = useQuery({
  placeholderData: []
})
Enter fullscreen mode Exit fullscreen mode

isLoadingError vs isRefetchError

// isLoadingError: Error on initial fetch
// isRefetchError: Error on background refetch

if (isLoadingError) return <FullPageError />
if (isRefetchError) return <ToastError />
Enter fullscreen mode Exit fullscreen mode

Fetch Status

fetchStatus

const { fetchStatus } = useQuery(...)

// 'fetching' → actively fetching
// 'paused'   → network offline
// 'idle'     → not fetching
Enter fullscreen mode Exit fullscreen mode

Control Properties

refetch

const { refetch } = useQuery(...)

// Manual refetch
<button onClick={() => refetch()}>
  Refresh
</button>

// Returns promise
const { data } = await refetch()
Enter fullscreen mode Exit fullscreen mode

promise

const { promise } = useQuery(...)

// Promise for current fetch
await promise  // Wait for data
Enter fullscreen mode Exit fullscreen mode

🔥 Real-World Production Patterns {#real-world-production-patterns}

✅ Dashboard

const { data } = useQuery({
  queryKey: ['dashboard'],
  queryFn: fetchDashboard,
  staleTime: 5 * 60 * 1000,     // 5 min fresh
  refetchInterval: 30 * 1000,    // Poll every 30s
  refetchOnWindowFocus: true,    // Sync on focus
})
Enter fullscreen mode Exit fullscreen mode

✅ Infinite Scroll List

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  staleTime: 60 * 1000,
  select: (data) => ({
    pages: data.pages.flatMap(page => page.items),
    pageParams: data.pageParams,
  }),
})
Enter fullscreen mode Exit fullscreen mode

✅ Dependent Queries

const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  enabled: !!userId,
})

const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchUserPosts(user.id),
  enabled: !!user?.id,
  staleTime: 2 * 60 * 1000,
})
Enter fullscreen mode Exit fullscreen mode

✅ Optimistic Updates

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (newUser) => {
    await queryClient.cancelQueries(['user', id])
    const previous = queryClient.getQueryData(['user', id])

    queryClient.setQueryData(['user', id], newUser)
    return { previous }
  },
  onError: (err, newUser, context) => {
    queryClient.setQueryData(['user', id], context.previous)
  },
  onSettled: () => {
    queryClient.invalidateQueries(['user', id])
  },
})
Enter fullscreen mode Exit fullscreen mode

🧬 Structural Sharing (Render Optimization)

This is a hidden performance weapon.

Problem:

// Every API response = new object reference
const data = await fetch('/api')
// React thinks it's different → re-render
Enter fullscreen mode Exit fullscreen mode

Solution:

React Query deep compares. If structure same → old reference reused → React skips re-render!

📊 Visual Example:

// First fetch
const data1 = { users: [{ id: 1, name: 'John' }] }

// Second fetch (same data, different object)
const data2 = { users: [{ id: 1, name: 'John' }] }

// Without structural sharing:
data1 === data2  // ❌ false → Component re-renders

// With structural sharing:
// React Query reuses data1 reference
// ✅ Component doesn't re-render!
Enter fullscreen mode Exit fullscreen mode

🎯 When to Disable:

{
  structuralSharing: false,
  // Disable when:
  // ❌ Huge deeply nested data (deep compare expensive)
  // ❌ Already normalized state (Redux-like structure)
  // ❌ Performance cost > benefit
}
Enter fullscreen mode Exit fullscreen mode

⚡ Performance Impact: For large lists (1000+ items), structural sharing can save 50-70% unnecessary re-renders!


⚠️ Common Mistakes {#common-mistakes}

❌ Unstable queryKey

// BAD: New object every render
queryKey: [{ userId: 1 }]

// GOOD: Serializable values
queryKey: ['user', 1]
Enter fullscreen mode Exit fullscreen mode

staleTime: 0 everywhere

// Causes excessive refetches
// Set appropriate staleTime based on data freshness needs
Enter fullscreen mode Exit fullscreen mode

❌ Polling without condition

// BAD: Always polling
refetchInterval: 5000

// GOOD: Conditional polling
refetchInterval: (data) => 
  data?.status === 'processing' ? 5000 : false
Enter fullscreen mode Exit fullscreen mode

❌ Overusing refetchOnWindowFocus

// Heavy queries should disable this
refetchOnWindowFocus: false
Enter fullscreen mode Exit fullscreen mode

❌ Not using select for transformations

// BAD: Transform in component
const userNames = data?.map(u => u.name)

// GOOD: Use select
select: (data) => data.map(u => u.name)
Enter fullscreen mode Exit fullscreen mode

🎯 When NOT to Use useQuery

❌ Pure client state

// Use useState/useReducer
const [count, setCount] = useState(0)
Enter fullscreen mode Exit fullscreen mode

❌ Form input local state

// Use form libraries or useState
const [email, setEmail] = useState('')
Enter fullscreen mode Exit fullscreen mode

❌ Animation state

// Use CSS or animation libraries
Enter fullscreen mode Exit fullscreen mode

Use useQuery for:

  • Server state
  • API data
  • Cached remote data
  • Background-synced data

🏗 Internal Architecture (High-Level) {#internal-architecture}

Under the hood:

QueryClient
  ├─ QueryCache (stores all queries)
  │   └─ Query (individual query)
  │       ├─ State (data, status, etc)
  │       └─ Observers (subscribers)
  │
  └─ MutationCache
Enter fullscreen mode Exit fullscreen mode

Flow:

  1. Component subscribes to query via QueryObserver
  2. Cache check happens
  3. Stale check happens
  4. Fetch triggered if needed
  5. Observer diff happens
  6. Only necessary re-render happens

This is why React Query works efficiently with React Fiber.


🔍 Advanced: Query Lifecycle

┌─────────────┐
│   Mount     │
└──────┬──────┘
       │
       ▼
┌─────────────┐     No Cache
│ Check Cache │────────────────┐
└──────┬──────┘                │
       │ Has Cache             │
       ▼                        ▼
┌─────────────┐          ┌──────────┐
│Check Stale  │          │  Fetch   │
└──────┬──────┘          └────┬─────┘
       │                      │
  Stale│    Fresh             │
       │      │               │
       ▼      ▼               ▼
    ┌────────────────────────────┐
    │   Return Data to Component  │
    └────────────────────────────┘
             │
             ▼
    ┌────────────────┐
    │ Background     │
    │ Refetch        │
    │ (if conditions │
    │  met)          │
    └────────────────┘
Enter fullscreen mode Exit fullscreen mode

🎯 Understanding the Flow:

  1. Component mounts → Query observer created
  2. Cache checked → Return if fresh
  3. Stale check → Fetch if needed
  4. Data rendered → Component subscribes
  5. Background refetch (based on triggers)
  6. Only changed state → Re-render

🎯 Performance Checklist {#performance-checklist}

✅ Set meaningful staleTime

staleTime: 5 * 60 * 1000  // 5 minutes for slow-changing data
Enter fullscreen mode Exit fullscreen mode

✅ Use select for transformations

select: (data) => data.users  // Only re-render when users change
Enter fullscreen mode Exit fullscreen mode

✅ Use notifyOnChangeProps for large states

notifyOnChangeProps: ['data']  // Only notify on data change
Enter fullscreen mode Exit fullscreen mode

✅ Enable structuralSharing (default)

structuralSharing: true  // Reuse references
Enter fullscreen mode Exit fullscreen mode

✅ Conditional polling

refetchInterval: (data) => data?.needsUpdate ? 5000 : false
Enter fullscreen mode Exit fullscreen mode

✅ Disable unnecessary refetch options

refetchOnWindowFocus: false,  // For heavy queries
refetchOnMount: false,        // If data won't change
Enter fullscreen mode Exit fullscreen mode

🔥 Key Takeaways

useQuery isn't just a data fetch hook. It is a:

  1. Cache manager (gcTime, staleTime)
  2. Background sync engine (refetchOnWindowFocus, refetchInterval)
  3. Retry system (retry, retryDelay)
  4. Stale controller (staleTime, isStale)
  5. Render optimizer (structuralSharing, notifyOnChangeProps)

If you're building a production React app — understanding useQuery is mandatory.


📌 Final Thought

React Query's real power shows when you combine:

  • staleTime
  • Background refetch
  • Structural sharing
  • Observer-based rendering
  • select transformations

Intelligently managing server state is the next step in modern React architecture.


🚀 Next Steps

  1. Read TanStack Query docs
  2. Try useMutation for mutations
  3. Explore useInfiniteQuery for pagination
  4. Setup React Query DevTools
  5. Learn query invalidation patterns

💡 Pro Tips

💡 Start with these three options first:

  • staleTime – Control freshness
  • select – Optimize re-renders
  • enabled – Conditional fetching

Master these before diving into advanced patterns.


🤝 Let's Connect

Found this helpful?

  • ❤️ Save this article for future reference
  • 🔄 Share with your team
  • 💬 Drop your questions in comments
  • 🐦 Follow me for more React content

What's your biggest challenge with React Query? Comment below! 👇


📚 Official Docs:

https://tanstack.com/query/latest/docs/react/overview


Happy querying! 🎯

Tags: #react #tanstack #reactquery #javascript #webdev #frontend #performance #statemanagement

Top comments (0)