Skip to content

Instantly share code, notes, and snippets.

@YuCJ
Created December 2, 2025 08:07
Show Gist options
  • Select an option

  • Save YuCJ/14de7e9239eae1776a5afdc5cb7bba64 to your computer and use it in GitHub Desktop.

Select an option

Save YuCJ/14de7e9239eae1776a5afdc5cb7bba64 to your computer and use it in GitHub Desktop.
isFetching, isLoading, and isPending

TanStack Query State Indicators: isFetching, isLoading, and isPending

Overview

Understanding the differences between isFetching, isLoading, and isPending is crucial for implementing effective loading states in TanStack Query (React Query) v5.

Definitions

1. isPending

  • true when the query has no cached data yet (awaiting first successful fetch)
  • Also true when the query is disabled (enabled: false)
  • Indicates "we don't have data yet"

2. isFetching

  • true whenever a fetch operation is in progress
  • Applies to initial fetch, refetch, background refetch, or pagination
  • Indicates "a network request is happening right now"

3. isLoading

  • Derived state: isLoading = isPending && isFetching
  • true only when fetching for the first time with no cached data
  • Indicates "initial fetch in progress"

Relationship

isLoading = isPending && isFetching

Common Query Flows and State Changes

Flow 1: Mount with enabled: false → API response success

Stage isPending isFetching isLoading
Initial mount (enabled: false) true false false
enabled changes to true, fetch starts true true true
API response success false false false

Use case: Conditionally enabling queries based on user input or dependencies.


Flow 2: Infinite query enabled: false → API response success → fetch more → API response success again

Stage isPending isFetching isLoading
Initial mount (enabled: false) true false false
enabled changes to true, initial fetch starts true true true
Initial fetch success false false false
fetchNextPage() called false true false
Additional fetch success false false false

Use case: Infinite scroll, pagination, "load more" buttons.

Key insight: isLoading is false during pagination because cached data exists.


Flow 3: Mount with enabled: true → re-render with enabled: falseenabled: true again → API response success

Stage isPending isFetching isLoading
Initial mount (enabled: true), fetch starts true true true
Fetch completes successfully false false false
enabled changes to false false* false false
enabled changes back to true false** true false
Refetch completes false false false

*Note: isPending stays false because cached data exists.

**Note: If cache is stale or invalidated, isPending may become true again.


Flow 4: Mount with enabled: true → API response success → background refetch

Stage isPending isFetching isLoading
Initial mount (enabled: true), fetch starts true true true
Initial fetch success false false false
Background refetch starts (e.g., window focus) false true false
Background refetch completes false false false

Use case: Automatic background data synchronization.


Flow 5: Query with cached data → refetch

Stage isPending isFetching isLoading
Component mounts with cached data false false false
refetch() called false true false
Refetch completes false false false

Use case: Manual refresh button.


Flow 6: Mount with enabled: true → Query key changes (new data needed)

Stage isPending isFetching isLoading
Initial mount, fetch starts true true true
Initial fetch success false false false
Query key changes (e.g., campaignId changes) true true true
New fetch completes false false false

Note: When query key changes, it's treated as a completely new query with no cache, so all states reset.


Flow 7: Mount with placeholderData: keepPreviousData → Query key changes

Stage isPending isFetching isLoading
Initial mount, fetch starts true true true
Initial fetch success (page 1) false false false
Query key changes (page 2), old data shown false true false
New fetch completes false false false

Note: keepPreviousData prevents isPending from becoming true when fetching new pages, providing smoother UX.


Flow 8: Query with staleTime: 0 (always refetch on mount)

Stage isPending isFetching isLoading
Component mounts with stale cache false true false
Refetch completes false false false
Component unmounts and remounts false true false
Refetch completes false false false

Use case: Always-fresh data requirements.


Flow 9: Query error → retry

Stage isPending isFetching isLoading
Initial mount, fetch starts true true true
Fetch fails, retry starts true true true
Retry succeeds false false false

Use case: Automatic retry logic.


Flow 10: Query with cache → invalidate → refetch

Stage isPending isFetching isLoading
Component mounts with valid cache false false false
queryClient.invalidateQueries() called false true false
Refetch completes false false false

Use case: Data invalidation after mutations.


When to Use Each State

Use isPending for:

  • ✅ Full-page loading spinners/skeletons on initial load
  • ✅ Showing "no data yet" states
  • ✅ Conditional rendering when data might not exist
if (isPending) {
  return <LoadingSpinner />;
}

Use isFetching for:

  • ✅ Subtle loading indicators during refetches
  • ✅ Disabling buttons/interactions during any fetch
  • ✅ Progress bars or inline spinners
  • ✅ Infinite scroll loading indicators
const shouldDisableButton = isFetching || isProcessing;

<button disabled={isFetching}>
  {isFetching ? <Spinner /> : 'Refresh'}
</button>

Use isLoading for:

  • ✅ Alternative to isPending when you specifically want to show loading only during active initial fetch
  • ✅ When you want to distinguish "no data + not fetching" from "no data + fetching"
if (isLoading) {
  return <LoadingSpinner />;
}

Real-world Usage Patterns from Codebase

Pattern 1: Initial loading screen

const { data: campaignData, isPending } = useGetCampaignsByIds(
  { campaignIds: campaignId ? [campaignId] : [] },
  { enabled: !!campaignId }
);

if (isPending) {
  return <LoadingSpinner />;
}

Pattern 2: Disable interactions during any fetch

const { isFetching } = useQuery(/* ... */);
const shouldDisableRefreshBtn = isFetching || isWaitingToRefetch;

Pattern 3: Show different loaders for initial vs. subsequent fetches

const { isPending, isFetchingNextPage } = useInfiniteQuery(/* ... */);
const shouldShowLoader = isWaitingToRefetch || (isPending && !isFetchingNextPage);

Pattern 4: Infinite scroll loading indicator

const { isFetching, hasNextPage, fetchNextPage } = useInfiniteQuery(/* ... */);

useInfiniteScroll({ 
  isLoading: isFetching, 
  hasMore: hasNextPage, 
  loadMore: fetchNextPage 
});

Pattern 5: Content area loading overlay

const { data, isFetching, isPending } = useQuery(/* ... */);

return (
  <>
    {isPending && <FullPageLoader />}
    {!isPending && data && (
      <>
        {isFetching && <LoadingOverlay />}
        <Content data={data} />
      </>
    )}
  </>
);

Key Takeaways

State Meaning Best For
isPending No data yet Full-page skeleton/spinner
isFetching Any fetch in progress Subtle loading indicators (button spinners, progress bars)
isLoading First fetch only Alternative to isPending (ensures active fetching)

Quick Decision Guide

Need to show loading on first render?
  → Use isPending

Need to show loading during refetch/background updates?
  → Use isFetching

Need to show loading ONLY during initial fetch (not when disabled)?
  → Use isLoading

Need to disable button during any fetch?
  → Use isFetching

Need to show "load more" spinner?
  → Use isFetching

Migration from v4 to v5

In TanStack Query v4, isLoading was used where isPending is now recommended in v5.

v4:

const { isLoading } = useQuery(/* ... */);

v5:

const { isPending } = useQuery(/* ... */);
// or
const { isLoading } = useQuery(/* ... */); // Still works, but isPending is clearer

The introduction of isPending makes the distinction between "no data yet" and "actively fetching" more explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment