Skip to main content
TanStack Query distinguishes between initial loading states and background refetching states, allowing you to provide appropriate feedback to users at each stage of data fetching.

Understanding Fetch States

Every query has two important state properties:
  • status - Reflects the data state: pending, error, or success
  • fetchStatus - Reflects the fetch state: fetching, paused, or idle
import { useQuery } from '@tanstack/react-query'

function Example() {
  const { status, fetchStatus, data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  // status: 'pending' | 'error' | 'success'
  // fetchStatus: 'fetching' | 'paused' | 'idle'
}
A query can be in success status while fetchStatus is fetching - this means you have data from a previous fetch, but a background refetch is in progress.

Primary Loading States

isPending

True when the query has no data yet:
function Posts() {
  const { isPending, data, error } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  if (isPending) {
    return <div>Loading...</div>
  }
  
  if (error) {
    return <div>Error: {error.message}</div>
  }
  
  return (
    <ul>
      {data.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

isLoading

True when the query is pending AND fetching:
function Posts() {
  const { isLoading, data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  // isLoading = isPending && isFetching
  if (isLoading) {
    return <Spinner />
  }
  
  return <PostList posts={data} />
}

Background Fetching Indicators

isFetching

True whenever a fetch is in progress (initial or background):
function Posts() {
  const { data, error, isFetching } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  return (
    <div>
      <h1>Posts</h1>
      {data && (
        <>
          <PostList posts={data} />
          {isFetching && (
            <div style={{ color: 'green' }}>
              Updating in background...
            </div>
          )}
        </>
      )}
    </div>
  )
}

Real-World Example

Complete example showing all states:
import { useQuery } from '@tanstack/react-query'

function GitHubRepo() {
  const { isPending, error, data, isFetching } = useQuery({
    queryKey: ['repoData'],
    queryFn: async () => {
      const response = await fetch(
        'https://api.github.com/repos/TanStack/query'
      )
      return await response.json()
    },
  })

  if (isPending) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.full_name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>{data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
      <div>{isFetching ? 'Updating...' : ''}</div>
    </div>
  )
}

Global Fetching Indicators

Show a global indicator when any query is fetching:
import { useIsFetching } from '@tanstack/react-query'

function GlobalLoadingIndicator() {
  const isFetching = useIsFetching()
  
  return isFetching ? (
    <div className="global-loading-bar">
      Loading...
    </div>
  ) : null
}

function App() {
  return (
    <>
      <GlobalLoadingIndicator />
      <YourApp />
    </>
  )
}

Filtered Global Indicators

Show indicators for specific query types:
import { useIsFetching } from '@tanstack/react-query'

function PostsLoadingIndicator() {
  // Only count fetching for post-related queries
  const isFetchingPosts = useIsFetching({ queryKey: ['posts'] })
  
  return isFetchingPosts > 0 ? (
    <div>Fetching {isFetchingPosts} post queries...</div>
  ) : null
}
useIsFetching returns the number of queries currently fetching, not just a boolean.

Visual Feedback Patterns

Inline Background Indicator

function Posts() {
  const { data, isFetching } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
        <h1>Posts</h1>
        {isFetching && (
          <div
            style={{
              width: 10,
              height: 10,
              background: 'green',
              borderRadius: '100%',
              animation: 'pulse 1s infinite',
            }}
          />
        )}
      </div>
      <PostList posts={data} />
    </div>
  )
}

Progress Bar

function ProgressBar({ show }) {
  return (
    <div
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        height: 3,
        background: 'linear-gradient(90deg, #3b82f6, #8b5cf6)',
        transform: show ? 'scaleX(1)' : 'scaleX(0)',
        transformOrigin: 'left',
        transition: show ? 'transform 0.3s ease' : 'transform 0.3s ease 0.2s',
      }}
    />
  )
}

function App() {
  const isFetching = useIsFetching()
  
  return (
    <>
      <ProgressBar show={isFetching > 0} />
      <YourApp />
    </>
  )
}

Refresh Indicator with Auto Refetch

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

function AutoRefetchExample() {
  const [intervalMs, setIntervalMs] = React.useState(1000)
  
  const { data, isFetching } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const response = await fetch('/api/data')
      return await response.json()
    },
    refetchInterval: intervalMs,
  })
  
  return (
    <div>
      <label>
        Refetch Interval (ms):{' '}
        <input
          type="number"
          value={intervalMs}
          onChange={(e) => setIntervalMs(Number(e.target.value))}
          step="100"
        />
        {' '}
        <span
          style={{
            display: 'inline-block',
            width: 10,
            height: 10,
            background: isFetching ? 'green' : 'transparent',
            transition: !isFetching ? 'all .3s ease' : 'none',
            borderRadius: '100%',
            transform: 'scale(2)',
          }}
        />
      </label>
      
      <ul>
        {data?.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  )
}

Mutation Loading States

Show loading states for mutations:
import { useMutation } from '@tanstack/react-query'

function CreatePost() {
  const mutation = useMutation({
    mutationFn: async (newPost) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost),
        headers: { 'Content-Type': 'application/json' },
      })
      return await response.json()
    },
  })
  
  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Post' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create Post'}
    </button>
  )
}

Global Mutation Indicator

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

function GlobalMutatingIndicator() {
  const isMutating = useIsMutating()
  
  return isMutating ? (
    <div className="saving-indicator">
      Saving changes...
    </div>
  ) : null
}

Combining States

Complete Loading UX

function Posts({ setPostId }) {
  const queryClient = useQueryClient()
  const { status, data, error, isFetching } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })
  
  return (
    <div>
      <h1>Posts</h1>
      
      {status === 'pending' ? (
        <div className="loading-skeleton">
          <Spinner /> Loading posts...
        </div>
      ) : status === 'error' ? (
        <div className="error-message">
          <span>Error: {error.message}</span>
        </div>
      ) : (
        <>
          <div>
            {data.map((post) => (
              <div key={post.id}>
                <a
                  onClick={() => setPostId(post.id)}
                  href="#"
                  style={{
                    fontWeight: queryClient.getQueryData(['post', post.id])
                      ? 'bold'
                      : 'normal',
                    color: queryClient.getQueryData(['post', post.id])
                      ? 'green'
                      : 'black',
                  }}
                >
                  {post.title}
                </a>
              </div>
            ))}
          </div>
          
          {isFetching && (
            <div className="background-update">
              Background Updating...
            </div>
          )}
        </>
      )}
    </div>
  )
}

Best Practices

1

Use isPending for initial loads

Show full loading states (spinners, skeletons) when isPending is true.
2

Use isFetching for background updates

Show subtle indicators (small spinners, progress bars) when isFetching is true but data exists.
3

Provide context-appropriate feedback

Global indicators for app-wide fetching, inline indicators for component-specific updates.
4

Don't over-indicate

Too many loading indicators can be distracting. Choose the most important ones to display.
5

Make indicators non-intrusive

Background refetch indicators should be subtle and not interrupt the user experience.
Avoid blocking the UI during background refetches. Users should be able to interact with cached data while fresh data loads.

Build docs developers (and LLMs) love