Polling in React with TanStack Query and refetchInterval
Poll APIs in React with TanStack Query: set up refetchInterval, add conditional polling and dynamic intervals, and avoid the common performance pitfalls.
Some data changes without the user doing anything. A build that is still running, an order working its way through fulfilment, a sensor reporting every few minutes. The screen has to keep up, and the simplest way to do that is to ask again on a timer.
TanStack Query makes this one option on a hook you are probably already using: refetchInterval. What follows is how to set it, the defaults that will surprise you, and the two mistakes that turn polling into a support ticket. The full option list is in the useQuery reference if you want it alongside.
Why Choose TanStack Query and refetchInterval for Polling?
Polling is how most dashboards read data that a device pushes on its own schedule. If the device end is what you are building, the ESP32 weather station covers publishing readings over MQTT from the hardware side. And if you are weighing polling against a push transport at all, the trade-offs are laid out in WebSockets vs SSE vs polling.
TanStack Query handles server state, meaning data that lives on a backend and needs syncing. If you are weighing it against a plain effect or SWR, start with fetching data in React. Server state is a different problem from the client state covered in React state management. Before we jump into the implementation, let's understand why it is an excellent choice for polling in React applications:
- Simplicity: TanStack Query abstracts away many of the complexities involved in data fetching, caching, and synchronization.
- Flexibility: It offers a range of options to customize polling behavior, from simple intervals to dynamic adjustments based on data freshness.
- Performance: With built-in caching and deduplication, TanStack Query ensures efficient network usage and optimal performance.
Now, let's get started with setting up your React environment for polling with TanStack Query.
Setting Up Your React Environment with TanStack Query
First things first, you'll need to install TanStack Query in your React project. You can do this using npm or yarn:
npm install @tanstack/react-query
# or
yarn add @tanstack/react-queryOnce installed, wrap your application with the QueryClientProvider from TanStack Query. This provider makes the query client available throughout your app, allowing components to fetch and manage data seamlessly.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
</QueryClientProvider>
);
}
export default App;With the setup complete, we're ready to implement polling in our React components.
Implementing Basic Polling with TanStack Query
TanStack Query's useQuery hook is your go-to tool for fetching data, and it supports polling right out of the box. To enable polling, simply set the refetchInterval option to specify the interval (in milliseconds) at which the data should be refetched.
Here's a basic example of polling an API endpoint every 5 seconds:
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchData = async () => {
const response = await axios.get('https://api.example.com/data');
return response.data;
};
const PollingComponent = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
refetchInterval: 5000, // Poll every 5 seconds
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Polling Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default PollingComponent;In this example, the PollingComponent fetches data from the specified API endpoint every 5 seconds, displaying the data or loading/error states as appropriate.
Advanced Polling Techniques
While basic polling is straightforward, TanStack Query offers several advanced options to fine-tune your polling behavior.
How do you poll conditionally?
You can control polling based on user activity or other conditions using the refetchOnWindowFocus option. This ensures that data is only fetched when the user is actively interacting with your app, reducing unnecessary network requests.
const { data, isLoading, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
refetchInterval: 5000,
refetchOnWindowFocus: true, // Refetch when window regains focus
});Can the polling interval change at runtime?
For scenarios where the polling interval should adjust based on data freshness or other factors, you can use a function for refetchInterval. This function receives the current data as an argument, allowing you to make informed decisions about when to refetch.
const { data, isLoading, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
refetchInterval: (data) => data?.needsRefresh ? 5000 : false, // Poll only if data needs refresh
});In this example, polling occurs every 5 seconds only if the data indicates a need for refresh. Otherwise, polling is paused.
Stop polling when the tab is hidden
This is the single most valuable addition to any polling setup, and it is one option. A background tab polling every five seconds burns the user's battery and your server capacity to update a screen nobody is looking at. TanStack Query pauses refetching for backgrounded tabs by default, but only if you leave refetchIntervalInBackground at its default of false.
const { data } = useQuery({
queryKey: ['prices'],
queryFn: fetchPrices,
refetchInterval: 5000,
// Default. Setting this to true is almost always a mistake:
// every backgrounded tab keeps hitting your API forever.
refetchIntervalInBackground: false,
});The exception is something the user genuinely needs updated while they are elsewhere, such as a build that should fire a notification on completion. Even then, consider whether the server should push instead.
Back off when requests fail
A fixed interval against a failing endpoint is a retry storm. If the API is down, every client hammers it every five seconds, which is exactly the load pattern that keeps it down. Because refetchInterval accepts a function, the interval can respond to failures:
const query = useQuery({
queryKey: ['status', jobId],
queryFn: () => fetchStatus(jobId),
refetchInterval: (q) => {
// Back off exponentially while the endpoint is failing.
if (q.state.status === 'error') {
return Math.min(60_000, 2 ** q.state.fetchFailureCount * 1000);
}
// Stop entirely once the job is finished.
return q.state.data?.done ? false : 3000;
},
});Returning false is how polling stops. Forgetting that is the most common bug in this pattern: a job-status poller that keeps querying a completed job for as long as the component stays mounted, which on a dashboard can be hours.
Match the interval to how fast the data actually changes
The instinct is to poll fast so the UI feels live. The cost is linear in clients: at one second, a thousand concurrent users generate a thousand requests per second, and almost all of them return data identical to the last response.
| Data | Reasonable interval |
|---|---|
| Background job or build status | 2 to 5 seconds, stopping on completion |
| Dashboard metrics | 30 to 60 seconds |
| Notification counts | 60 seconds or more |
| Prices, live scores | A few seconds, and consider a push transport instead |
Below roughly a second, polling stops being the right tool. The comparison with WebSockets and Server-Sent Events, including why SSE is usually the better upgrade, is in WebSockets vs SSE vs polling.
Set staleTime, or the interval is not the only fetch
A frequent surprise: the data refetches more often than the interval suggests. That is because refetchInterval is not the only trigger. Window focus, network reconnect and component remounts all refetch by default when data is stale, and the default staleTime is zero, meaning always stale.
useQuery({
queryKey: ['metrics'],
queryFn: fetchMetrics,
refetchInterval: 30_000,
// Treat data as fresh for most of the interval, so tab focus and
// remounts do not add a second stream of requests on top.
staleTime: 25_000,
});Without this, tabbing away and back fires an immediate extra request every time, which on a dashboard people switch between constantly can double or triple the real request volume.
Best Practices for Polling with TanStack Query
To ensure your polling implementation is efficient and user-friendly, consider the following best practices:
- Error Handling: Always handle errors gracefully by displaying user-friendly messages or fallback content.
- Optimize Network Usage: Adjust polling intervals based on the criticality of data freshness. For less critical data, consider longer intervals to reduce network load.
- Leverage DevTools: Use TanStack Query DevTools to monitor and debug your queries, ensuring everything is working as expected.
Conclusion
Polling is a powerful technique for fetching real-time data in React applications, and TanStack Query makes it easier than ever to implement. With its simple setup, flexible options, and robust performance optimizations, TanStack Query is an excellent choice for developers looking to add real-time data fetching to their React apps.
By following the guidelines and examples provided in this article, you're well-equipped to start implementing polling in your React projects today. Happy coding!
Further Reading
By incorporating these resources and continuing to explore TanStack Query's features, you'll be able to take your React applications to the next level with efficient, real-time data fetching.
Frequently asked questions
How do I poll an API with TanStack Query?
Set refetchInterval on useQuery to a number of milliseconds. That is the whole feature: refetchInterval: 5000 refetches every five seconds and hands you fresh data through the same query result you already render. Caching, deduplication and error handling come along with it, so two components polling the same key produce one request rather than two. The interval is measured from when the previous request settles rather than from when it started, which means a slow endpoint cannot stack overlapping requests the way a bare setInterval will. That property is most of the reason to use the library instead of writing the loop by hand. The other reason is that the polling state and the render state are the same object, so there is no effect to clean up and no stale closure holding an old value.
Does polling keep running when the tab is in the background?
Not by default, and you should leave it that way. refetchIntervalInBackground defaults to false, so a backgrounded tab stops polling and resumes on focus. Setting it to true burns battery and server capacity updating a screen nobody is looking at, and that cost multiplies across every tab a user has forgotten about. The exception is something the user needs to complete while they are elsewhere, and even then a push from the server is usually the better answer. One consequence worth knowing: a user returning to a stale tab would otherwise see old data. refetchOnWindowFocus is already true by default, so a refetch fires the moment they come back, and what they see is a loading state over existing data rather than an empty screen. Mobile browsers may also suspend timers in a backgrounded tab regardless of this setting, so never rely on background polling for correctness.
Can the polling interval change based on the data?
Yes. refetchInterval accepts a function that receives the current query result, so you can return a different interval, or return false to stop polling entirely. This is how you poll a job hard until it finishes and then stop, and how you back off when requests start failing. A fixed interval against a failing endpoint is a retry storm: fifty clients hitting a down service every two seconds is exactly the load that keeps it down. The function runs after each fetch settles, so returning false the moment a job reports a terminal state means no further requests and no cleanup code of your own. The pattern worth copying is an interval that starts short, grows on consecutive failures, and returns false on completion, all expressed as one function rather than as state you maintain separately.
Should I poll or use WebSockets?
Poll first. Polling is stateless, survives proxies and load balancers without configuration, and costs nothing to reason about when a request fails. It stops being the right answer when the interval you need drops under a second or so, or when most polls come back reporting nothing changed. At that point the request overhead is the problem and a push transport earns its operational cost. Do the arithmetic before switching: a five second poll is twelve requests per user per minute, which is trivial at hundreds of users and a real load at a hundred thousand. Server-Sent Events are usually the next step rather than WebSockets, because the traffic is one-directional and SSE reconnects on its own. Reach for WebSockets when the client genuinely needs to send as well as receive. Whichever you land on, the server still needs a rate limit, because a client stuck in a reconnect loop is indistinguishable from an attack.


