Skip to main content

React Query Stale Data: Refetch Strategies and Control

Refetching is the process of re-running a query's fetcher function to get fresh data from the server. TanStack Query refetches automatically in response to events (window focus, network reconnect, component mount) and can be manually triggered by users or mutations. Understanding when and how to refetch is crucial for balancing freshness with unnecessary requests. This article covers automatic refetch triggers, polling strategies, and manual control patterns that keep your app responsive and up-to-date.

I once deployed a dashboard where the data never updated unless the user manually refreshed the page. Adding refetchOnWindowFocus: true and refetchOnReconnect: true immediately made the product feel alive—when users switched tabs or regained internet, their dashboards silently refreshed without any user action. It was a small change with outsized impact.

When Does a Query Refetch Automatically?

TanStack Query refetches stale data (data past its staleTime) in response to several events:

EventConfigBehavior
Component mountsrefetchOnMount (default: true)If data is stale and a component using the query mounts, refetch in background
Window focusrefetchOnWindowFocus (default: true)If user switches to your tab and data is stale, refetch
Network reconnectrefetchOnReconnect (default: true)If connection drops and reconnects and data is stale, refetch
Time-based pollingrefetchInterval (default: undefined)Re-run fetcher every N milliseconds, regardless of staleness
Manual triggerrefetch() functionComponent calls refetch() to immediately re-run the fetcher
const { data: user, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 1000 * 60, // 1 minute
refetchOnMount: true, // Default
refetchOnWindowFocus: true, // Default
refetchOnReconnect: true, // Default
// refetchInterval: 1000 * 30, // Polling every 30 sec (optional)
});

// Manually trigger refetch
const handleRefresh = () => refetch();

Leveraging Window Focus for Zero-Effort Freshness

refetchOnWindowFocus: true is one of the library's best features for dashboards. When a user switches to your tab after checking email or working in another app, TanStack Query automatically refetches stale data in the background. The old data is displayed while new data arrives, so there is no jarring loading spinner.

function StockTicker({ symbol }) {
const { data: price, isFetching } = useQuery({
queryKey: ['stock', symbol],
queryFn: async () => {
const res = await fetch(`/api/stocks/${symbol}`);
return res.json();
},
staleTime: 1000 * 30, // 30 seconds
refetchOnWindowFocus: true, // Auto-refresh on tab switch
});

return (
<div>
<p>Price: ${price?.value}</p>
{isFetching && <small>Updating...</small>}
</div>
);
}

This is particularly effective for:

  • Financial dashboards (stock prices, crypto)
  • Real-time metrics (server stats, queue depths)
  • Notifications (unread counts, alerts)
  • Collaborative apps (seeing others' edits)

Users expect these to be current when they return to the page, and this single flag delivers that experience without extra code.

Polling with refetchInterval

For data that must be constantly fresh (without user interaction), use refetchInterval:

function LiveMetrics() {
const { data: metrics } = useQuery({
queryKey: ['metrics'],
queryFn: async () => {
const res = await fetch('/api/metrics');
return res.json();
},
refetchInterval: 1000 * 5, // Poll every 5 seconds
refetchIntervalInBackground: false, // Stop polling if tab is hidden
});

return <div>Requests: {metrics?.requestsPerSecond}</div>;
}

Setting refetchInterval enables polling: the fetcher re-runs automatically at the specified interval. Setting refetchIntervalInBackground: false pauses polling when the user switches tabs (battery and bandwidth savings).

Be cautious with polling: 10 components each polling every 5 seconds = 120 requests per minute. Consider:

  • High refetchInterval values (30–60 seconds) for less critical data
  • Server-sent events (SSE) or WebSockets for real-time push instead of polling
  • Conditional polling: only poll when necessary (user is looking at the page, network is available)

Manual Refetch Triggers

Use the refetch() function for user-initiated refreshes:

function UserList() {
const { data: users, refetch, isFetching } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('/api/users');
return res.json();
},
staleTime: 1000 * 60 * 5, // 5 minutes
});

return (
<div>
<button
onClick={() => refetch()}
disabled={isFetching}
>
{isFetching ? 'Refreshing...' : 'Refresh List'}
</button>
<ul>
{users?.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
);
}

The refetch() function immediately re-runs the fetcher without waiting for staleTime to expire. This is useful for:

  • Manual "Refresh" buttons
  • Retry buttons after errors
  • User-initiated data syncs

You can also pass options to refetch():

// Refetch and show a loading skeleton
await refetch({ throwOnError: true });

// Refetch in the background without updating the UI until ready
refetch({ cancelRefetch: false });

Refetching Multiple Queries at Once

Use queryClient.refetchQueries() to refetch all queries matching a pattern:

function useRefreshAllData() {
const queryClient = useQueryClient();

return async () => {
// Refetch all queries
await queryClient.refetchQueries();

// Refetch all user queries
await queryClient.refetchQueries({
queryKey: ['user'],
exact: false, // Match keys starting with ['user']
});

// Refetch only when data is stale
await queryClient.refetchQueries({
queryKey: ['posts'],
type: 'stale', // Only refetch if stale (default: 'all')
});
};
}

function AppHeader() {
const refreshAll = useRefreshAllData();

return (
<header>
<button onClick={refreshAll}>Sync All Data</button>
</header>
);
}

The type: 'stale' option only refetches data that has exceeded staleTime, avoiding unnecessary requests for fresh data.

Handling Network Status Changes

TanStack Query automatically refetches stale data when the network reconnects:

function UserProfile() {
const { data: user, status } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
refetchOnReconnect: true, // Auto-refetch on reconnect (default)
});

if (status === 'pending') return <Skeleton />;
if (status === 'error') return <Error />;

return <div>{user.name}</div>;
}

For apps that are heavily used on mobile (where network changes are common), this is critical. A user loses WiFi, regains 4G, and the app silently refetches without user interaction.

To detect offline/online status explicitly:

function useOnlineStatus() {
const [isOnline, setIsOnline] = React.useState(
typeof navigator !== 'undefined' ? navigator.onLine : true
);

React.useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);

window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);

return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);

return isOnline;
}

// Use in a component
function App() {
const isOnline = useOnlineStatus();

return (
<div>
{!isOnline && <OfflineBar />}
<MainContent />
</div>
);
}

Preventing Unnecessary Refetches

Sometimes you want stale data to persist without refetching. For example, a user navigates to a user detail page and back to the list: the list should not refetch because the user is only briefly gone.

const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnMount: false, // Don't refetch on mount
refetchOnWindowFocus: false, // Don't refetch on window focus
});

This is aggressive, but useful for static lists or when you are manually managing freshness via mutations.

Refetch in Error Scenarios

Combine refetch with error handling for resilient UIs:

function DataDisplay() {
const { data, error, isError, refetch, isRefetching } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 2, // Retry twice before showing error
});

if (isError) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={() => refetch()} disabled={isRefetching}>
{isRefetching ? 'Trying again...' : 'Retry'}
</button>
</div>
);
}

return <div>{data}</div>;
}

The retry: 2 option retries the fetcher twice before marking it as failed. refetch() gives users a manual retry button that re-runs the fetcher without needing to reload the page.

Key Takeaways

  • refetchOnWindowFocus: true (default) auto-refetches stale data when the user returns to your tab.
  • refetchOnReconnect: true (default) auto-refetches when network connectivity is restored.
  • refetchInterval enables polling; combine with refetchIntervalInBackground: false for battery savings.
  • The refetch() function manually triggers a refetch; use it for user-initiated refresh buttons.
  • queryClient.refetchQueries() refetches all queries matching a pattern; use type: 'stale' to refetch only stale data.
  • Set refetchOnMount: false and refetchOnWindowFocus: false for data that should remain cached longer.

Frequently Asked Questions

Should I use refetchInterval or a WebSocket?

Use polling for non-critical, low-frequency updates (metrics refreshed every 30+ seconds). Use WebSocket or Server-Sent Events (SSE) for real-time, high-frequency, or critical updates (stock ticks, live comments, notifications). Polling is simpler but generates more traffic; WebSocket is lower latency and bandwidth.

Does refetchOnWindowFocus fire even if data is fresh?

No. refetchOnWindowFocus only triggers a refetch if the data has exceeded staleTime. If data is still fresh (within staleTime), no refetch occurs. This prevents unnecessary requests for recently fetched data.

Can I disable refetching for a specific query but enable it globally?

Yes. Set refetchOnMount: false, refetchOnWindowFocus: false, and refetchOnReconnect: false on the individual query. This overrides the global defaults set on QueryClient.

What is the difference between refetch() and invalidateQueries()?

refetch() immediately re-runs the fetcher and updates the UI with new data. invalidateQueries() marks the cache entry as stale; the actual refetch is deferred until a component mounts or the query is accessed again. Use refetch() for immediate action; use invalidateQueries() after mutations to trigger background refetches.

How do I cancel a pending refetch?

Call refetch({ cancelRefetch: true }) to cancel in-flight requests. The previous cached data is displayed. This is useful if the user navigates away before a slow refetch completes.

Further Reading