TanStack Query (React Query) - Quick overview ๐Ÿš€

Abheeshta PAbheeshta P
4 min read

Why TanStack Query?

Without it, youโ€™d manually:

  • Fetch data inside useEffect

  • Use useState to manage data, loading, and error

  • 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'
  1. 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 data

  • error โ€“ error object if any

  • isLoading โ€“ true during the first load

  • isFetching โ€“ 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 to queryFn (default is 1).

  • 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 (controls hasNextPage).

  • 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.

  1. 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

0
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.