Understanding the differences between isFetching, isLoading, and isPending is crucial for implementing effective loading states in TanStack Query (React Query) v5.
truewhen the query has no cached data yet (awaiting first successful fetch)- Also
truewhen the query is disabled (enabled: false) - Indicates "we don't have data yet"
truewhenever a fetch operation is in progress- Applies to initial fetch, refetch, background refetch, or pagination
- Indicates "a network request is happening right now"
- Derived state:
isLoading = isPending && isFetching trueonly when fetching for the first time with no cached data- Indicates "initial fetch in progress"
isLoading = isPending && isFetching| 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: false → enabled: 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
- ✅ Full-page loading spinners/skeletons on initial load
- ✅ Showing "no data yet" states
- ✅ Conditional rendering when data might not exist
if (isPending) {
return <LoadingSpinner />;
}- ✅ 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>- ✅ Alternative to
isPendingwhen 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 />;
}const { data: campaignData, isPending } = useGetCampaignsByIds(
{ campaignIds: campaignId ? [campaignId] : [] },
{ enabled: !!campaignId }
);
if (isPending) {
return <LoadingSpinner />;
}const { isFetching } = useQuery(/* ... */);
const shouldDisableRefreshBtn = isFetching || isWaitingToRefetch;const { isPending, isFetchingNextPage } = useInfiniteQuery(/* ... */);
const shouldShowLoader = isWaitingToRefetch || (isPending && !isFetchingNextPage);const { isFetching, hasNextPage, fetchNextPage } = useInfiniteQuery(/* ... */);
useInfiniteScroll({
isLoading: isFetching,
hasMore: hasNextPage,
loadMore: fetchNextPage
});const { data, isFetching, isPending } = useQuery(/* ... */);
return (
<>
{isPending && <FullPageLoader />}
{!isPending && data && (
<>
{isFetching && <LoadingOverlay />}
<Content data={data} />
</>
)}
</>
);| 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) |
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
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 clearerThe introduction of isPending makes the distinction between "no data yet" and "actively fetching" more explicit.