Keeping Frontend Data Fresh Understanding TanStack Query's Automatic Sync
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Introduction
In the dynamic world of web development, ensuring that the data displayed to the user is always up-to-date and consistent with the backend server is a persistent challenge. Traditional approaches often involve manual data fetching, complex state management, or frequent, potentially inefficient, polling mechanisms. These methods can lead to stale UI, increased server load, and a less-than-ideal user experience. This is where modern data fetching libraries like TanStack Query (formerly React Query) shine. Designed with developer experience and performance in mind, TanStack Query offers powerful features for managing server state, particularly its intelligent capabilities for automatically synchronizing and refreshing data in the background. Understanding how it achieves this automatic synchronization is crucial for building robust, responsive, and efficient frontend applications.
Core Concepts of TanStack Query
Before diving into the specifics of automatic synchronization, let's establish a foundational understanding of a few key concepts within TanStack Query:
- Query: Represents an asynchronous request to fetch data (e.g.,
useQuery). It uniquely identifies the data using a "query key." - Query Key: A unique identifier (an array or a string) for each piece of cached data. This is how TanStack Query knows what data is what.
- Cache: TanStack Query maintains an in-memory cache of resolved queries. When you request data with a specific query key, TanStack Query first checks its cache. If the data is available and not stale, it's returned instantly.
- Stale Time: The duration after which cached data is considered "stale." Once data is stale, TanStack Query will attempt to refetch it in the background the next time it's accessed or observed.
- Cache Time: The duration after which inactive cached query data is garbage collected. If a query component unmounts and the data is no longer observed, it will remain in the cache until
cacheTimeexpires. - Refetching: The process of re-executing a query function to retrieve the latest data from the backend. This can happen automatically or be triggered manually.
- Background Refetching: When TanStack Query refetches data without showing a loading spinner on the initial render, making the UI feel snappier. Users see stale data for a brief moment before the fresh data arrives.
The Principle of Automatic Synchronization
TanStack Query's automatic synchronization is built on the principle of "stale-while-revalidate." This means that when a query's data is considered stale, TanStack Query will:
- Immediately return the cached (stale) data to the UI.
- In the background, initiate a refetch operation to get the latest data from the server.
- Once the refetch completes, it updates the cache with the new data and re-renders the UI with the fresh information.
This elegant mechanism prevents UI flickering from loading states and provides users with an instant response, even if the data might be momentarily out of date.
Mechanisms for Automatic Data Refresh
TanStack Query employs several intelligent mechanisms to automatically refetch and synchronize your data without explicit polling logic:
1. Refetch on Window Focus
This is perhaps one of the most powerful and often overlooked features. When a user navigates away from your application (e.g., switches tabs, minimizes the window) and then returns, TanStack Query automatically refetches all active and stale queries. This ensures that when the user re-engages with your app, they are presented with the latest data, bridging the gap from potential backend updates that occurred while they were away.
Example:
import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; interface Todo { id: number; title: string; completed: boolean; } const fetchTodos = async (): Promise<Todo[]> => { const { data } = await axios.get<Todo[]>('/api/todos'); return data; }; function TodoList() { const { data, isLoading, error } = useQuery<Todo[], Error>({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5 * 60 * 1000, // Data is considered stale after 5 minutes // By default, refetchOnWindowFocus is true }); if (isLoading) return <div>Loading todos...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <ul> {data?.map(todo => ( <li key={todo.id}>{todo.title} - {todo.completed ? 'Done' : 'Pending'}</li> ))} </ul> ); } export default TodoList;
In this example, if the user leaves the tab where TodoList is mounted for more than 5 minutes (staleTime), and then returns, TanStack Query will automatically refetch the todos in the background.
2. Refetch on Mount
When an instance of a useQuery hook mounts, if its data is stale, TanStack Query will automatically initiate a background refetch. This is useful for components that are frequently mounted and unmounted, ensuring they always display fresh data.
3. Refetch on Reconnect
If a user's network connection drops and then recovers, TanStack Query intelligently detects this and automatically refetches all active and stale queries. This feature significantly improves the user experience in environments with intermittent connectivity, as the application automatically "heals" itself upon reconnection.
4. Refetch on Interval (Polling)
While TanStack Query aims to reduce the need for manual polling, it provides an option to refetch data at specified intervals for use cases where near real-time updates are critical and other mechanisms aren't sufficient.
Example:
import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; interface StockPrice { symbol: string; price: number; timestamp: string; } const fetchStockPrice = async (symbol: string): Promise<StockPrice> => { const { data } = await axios.get<StockPrice>(`/api/stock/${symbol}`); return data; }; function StockPriceDisplay({ symbol }: { symbol: string }) { const { data, isLoading, error } = useQuery<StockPrice, Error>({ queryKey: ['stockPrice', symbol], queryFn: () => fetchStockPrice(symbol), refetchInterval: 5000, // Refetch every 5 seconds staleTime: Infinity, // Data never goes stale on its own, only by interval }); if (isLoading) return <div>Loading stock price...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <div> <h3>{data?.symbol} Price: ${data?.price.toFixed(2)}</h3> <p>Last updated: {new Date(data?.timestamp || '').toLocaleTimeString()}</p> </div> ); } export default StockPriceDisplay;
Here, the stockPrice for a given symbol will be refetched every 5 seconds, providing a near real-time update. Notice staleTime: Infinity is used because the refetchInterval is the primary mechanism for freshness here.
5. Manual Invalidations and Mutations
While the above mechanisms handle automatic background synchronization, there are times when you need to explicitly tell TanStack Query that certain data on the server has changed and needs to be refetched. This is particularly relevant after performing actions like creating, updating, or deleting resources.
queryClient.invalidateQueries(queryKey): This is a powerful method to mark queries matching a queryKey as stale. Once invalidated, the next time those queries are observed/accessed, they will be refetched in the background.
Example with Mutation:
import { useMutation, useQueryClient } from '@tanstack/react-query'; import axios from 'axios'; interface NewTodo { title: string; } interface CreatedTodo extends NewTodo { id: number; } const createTodo = async (newTodo: NewTodo): Promise<CreatedTodo> => { const { data } = await axios.post<CreatedTodo>('/api/todos', newTodo); return data; }; function AddTodoForm() { const queryClient = useQueryClient(); const mutation = useMutation<CreatedTodo, Error, NewTodo>({ mutationFn: createTodo, onSuccess: () => { // Invalidate the 'todos' query after a successful creation // This will cause the TodoList component to refetch its data queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget as HTMLFormElement); const title = formData.get('title') as string; mutation.mutate({ title }); }; return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="New todo title" /> <button type="submit" disabled={mutation.isPending}> {mutation.isPending ? 'Adding...' : 'Add Todo'} </button> {mutation.isError && <div>Error adding todo: {mutation.error?.message}</div>} </form> ); } export default AddTodoForm;
In this scenario, after a new todo is successfully created through AddTodoForm, queryClient.invalidateQueries({ queryKey: ['todos'] }) is called. This tells TanStack Query that the data associated with the ['todos'] key (which our TodoList uses) is now stale. The TodoList will then automatically trigger a background refetch to display the newly added todo without requiring a page refresh or manual state updates.
Conclusion
TanStack Query fundamentally changes how we manage server state in frontend applications, moving away from manual data synchronization towards an intelligent, declarative approach. By leveraging mechanisms like refetch on window focus, mount, reconnect, and flexible invalidation strategies, it ensures that your application's UI remains consistently synchronized with the backend data. This leads to a significantly improved user experience with responsive interfaces, reduced boilerplate code for developers, and ultimately, more performant and maintainable applications. Embracing TanStack Query's automated synchronization capabilities is a key step towards building truly modern and dynamic web experiences.

