TanStack Query (React Query) - Quick overview ๐


Why TanStack Query?
Without it, youโd manually:
Fetch data inside
useEffect
Use
useState
to managedata
,loading
, anderror
Handle refetching, caching, and side-effects on your own
With TanStack Query, it's all streamlined with a single hook โ useQuery
.
Caching & Refetching
Initial query result is cached (default: 5 minutes).
On revisits, it shows cached data instantly while silently fetching fresh data in the background.
For constant data, use:
staleTime: 60000 // 60 seconds, no background refetching within this time
Developer Tools
Install to visualize queries and cache:
npm i @tanstack/react-query-devtools
Usage:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
useQuery
for GET
const { data, error, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: async () => await axios.get('/posts')
});
Parameters:
queryKey
: A unique identifier for the query (also used for caching).Example:
['posts']
for/posts
['posts', postId, 'comments']
for/posts/1/comments
queryFn
: Async function where the actual API call happens. Returns a Promise.
Destructured Results:
data
โ response dataerror
โ error object if anyisLoading
โtrue
during the first loadisFetching
โtrue
while fetching in the background
1. Polling (Refetch Automatically)
Useful in real-time apps (e.g., trading platforms like Groww/Zerodha):
useQuery({
queryKey: ['stocks'],
queryFn: fetchStocks,
refetchInterval: 5000, // Refetch every 5 seconds
refetchIntervalInBackground: true // to poll even when in different tab
});
2. OnClick Refetch (Manual Trigger)
To fetch data only on button click:
const { data, refetch } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
enabled: false // disable auto-fetch on mount
});
<button onClick={() => refetch()}>Load Data</button>
3. Query by ID
useQuery({
queryKey: ['post', post.id],
queryFn: () => fetchPost(post.id)
});
4. Pagination with Placeholder Data
To handle pagination:
Limit your response manually in your API
Use
placeholderData
to prevent flickering
useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: prevPageData
});
5. Infinite Scroll with useInfiniteQuery
Use useInfiniteQuery
to load paginated data incrementally โ like social feeds or product lists.
Core Flow
const {
data,
fetchNextPage,
hasNextPage
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetch(`/posts?page=${pageParam}`).then(res => res.json()),
getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextPage : undefined
});
pageParam
: passed automatically toqueryFn
(default is1
).getNextPageParam
: decides the next page to fetch using the latest API response (lastPage
).
How getNextPageParam
Works
Runs after each fetch to tell TanStack the next
pageParam
to pass.If it returns
undefined
, no more pages are fetched (controlshasNextPage
).You must extract something like
lastPage.nextPage
or a cursor.
Example response:
{
"posts": [...],
"nextPage": 2,
"hasMore": true
}
Example logic:
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.nextPage : undefined
This setup powers infinite scroll with either a Load More button or an IntersectionObserver.
useMutation
for POST/PUT/DELETE
const mutation = useMutation({
mutationFn: (newPost) => axios.post('/posts', newPost),
onSuccess: (data) => {
// If this is not there we might have to use seperate button to ask user for refetch after posting
queryClient.invalidateQueries(['posts']); // this causes the clearing of the posts cache and fetch again from db
}
});
// Usage:
mutation.mutate({ title: 'New Post' });
Returns:
data
,error
,isLoading
, etc.onSuccess
,onError
,onSettled
1. Manual Cache Update
Instead of fetching again after POST, update local cache directly with returned data from post reuqest:
queryClient.setQueryData(['posts'], (oldData) => {
return {
...oldData,
data: [...oldData.data, newPost]
};
});
Make sure your queryKey matches exactly with the one used in useQuery
.
2. Optimistic Updates
Optimistically update UI before server confirms:
const mutation = useMutation({
mutationFn: postItem,
onMutate: async (newItem) => {
await queryClient.cancelQueries(['posts']); // cancels requests which are on going on posts
const prevData = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old) => ({
...old,
data: [...old.data, { ...newItem, id: old.data.length + 1 }]
}));
return { prevData };
},
onError: (err, newItem, context) => {
queryClient.setQueryData(['posts'], context.prevData);
},
onSettled: () => {
queryClient.invalidateQueries(['posts']);
}
});
๐น onMutate
runs before the mutation ๐น onError
handles rollback if mutation fails ๐น onSettled
ensures server data sync
Reminder:
setQueryData
only updates the local client cache, not the real database.
Thanks to Tanstack query
Subscribe to my newsletter
Read articles from Abheeshta P directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abheeshta P
Abheeshta P
I am a Full-stack dev turning ideas into sleek, functional experiences ๐. I am passionate about AI, intuitive UI/UX, and crafting user-friendly platforms . I am always curious โ from building websites to diving into machine learning and under the hood workings โจ. Next.js, Node.js, MongoDB, and Tailwind are my daily tools. I am here to share dev experiments, lessons learned, and the occasional late-night code breakthroughs. Always evolving, always building.