Skip to main content
Ganesh Joshi
Back to Blogs

TanStack Query: smarter data fetching in React

February 19, 20265 min read
Tutorials
TanStack Query data fetching and caching code on screen

TanStack Query (formerly React Query) transforms how you handle server state in React. Instead of managing loading states, error handling, caching, and refetching manually, you declare what data you need and let the library handle the rest.

The problem it solves

Manual data fetching requires managing:

// Without TanStack Query
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setIsLoading(false));
  }, [userId]);

  if (isLoading) return <Spinner />;
  if (error) return <Error />;
  return <Profile user={user} />;
}

TanStack Query simplifies this:

// With TanStack Query
function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error />;
  return <Profile user={user} />;
}

Plus you get caching, deduplication, background refetching, and more.

Installation

npm install @tanstack/react-query

Set up the provider:

// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export function Providers({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

useQuery basics

import { useQuery } from '@tanstack/react-query';

function TodoList() {
  const {
    data,        // The fetched data
    isLoading,   // First load, no data yet
    isFetching,  // Any fetch in progress
    error,       // Error object if failed
    refetch,     // Function to manually refetch
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  // ...
}

Query keys

Query keys identify cached data. Use arrays for hierarchical keys:

// All todos
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Todos filtered by status
useQuery({
  queryKey: ['todos', { status: 'completed' }],
  queryFn: () => fetchTodos({ status: 'completed' }),
});

// Single todo by ID
useQuery({
  queryKey: ['todos', todoId],
  queryFn: () => fetchTodo(todoId),
});

// Nested user data
useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
});

Caching and staleness

Concept Default Description
staleTime 0 How long data stays fresh
gcTime 5 minutes How long unused data stays in cache
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 1000 * 60 * 5,  // 5 minutes
  gcTime: 1000 * 60 * 30,    // 30 minutes
});
  • Fresh data: Won't refetch on mount or window focus
  • Stale data: Shown immediately, refetch happens in background
  • Garbage collected: Removed from cache, next access triggers fresh fetch

Automatic refetching

TanStack Query refetches stale data automatically:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: true,    // When user returns to tab
      refetchOnReconnect: true,      // When network reconnects
      refetchOnMount: true,          // When component mounts
      retry: 3,                      // Retry failed requests
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

useMutation

Handle create, update, delete operations:

import { useMutation, useQueryClient } from '@tanstack/react-query';

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

  const mutation = useMutation({
    mutationFn: (newTodo) => createTodo(newTodo),
    onSuccess: () => {
      // Invalidate and refetch todos
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ title: inputValue });
    }}>
      <input value={inputValue} onChange={...} />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  );
}

Optimistic updates

Update UI before the server confirms:

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // Snapshot previous value
    const previousTodos = queryClient.getQueryData(['todos']);

    // Optimistically update
    queryClient.setQueryData(['todos'], (old) =>
      old.map((todo) =>
        todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
      )
    );

    // Return context for rollback
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // Refetch to ensure server state
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Infinite queries

Load paginated data:

import { useInfiniteQuery } from '@tanstack/react-query';

function InfiniteTodos() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['todos', 'infinite'],
    queryFn: ({ pageParam = 0 }) => fetchTodosPage(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  return (
    <>
      {data.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.todos.map((todo) => (
            <TodoItem key={todo.id} todo={todo} />
          ))}
        </React.Fragment>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </>
  );
}

Dependent queries

Fetch data that depends on other data:

function UserPosts({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!userQuery.data, // Only run when user is loaded
  });
}

Parallel queries

Fetch multiple resources simultaneously:

import { useQueries } from '@tanstack/react-query';

function Dashboard({ userIds }) {
  const userQueries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
  });

  const isLoading = userQueries.some((q) => q.isLoading);
  const users = userQueries.map((q) => q.data);
}

DevTools

Add the DevTools for debugging:

npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

With Next.js

In the App Router, use TanStack Query in Client Components:

// app/posts/page.tsx
import { PostList } from './PostList';
import { prefetchPosts } from '@/lib/queries';

export default async function PostsPage() {
  // Optional: prefetch on server
  return <PostList />;
}

// app/posts/PostList.tsx
'use client';

import { useQuery } from '@tanstack/react-query';

export function PostList() {
  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
  // ...
}

Summary

TanStack Query simplifies server state management with automatic caching, deduplication, and refetching. Use useQuery for fetching, useMutation for updates, and query keys for cache organization. Configure staleTime based on data freshness needs. Invalidate queries after mutations to keep UI in sync. Add DevTools for debugging cache state.

Frequently Asked Questions

TanStack Query (formerly React Query) is a library for fetching, caching, and synchronizing server state in React. It handles loading, error states, caching, background refetching, and cache invalidation.

Use TanStack Query when you need caching, deduplication, background refetching, or shared data across components. Use useEffect for simple one-off fetches that don't need caching.

staleTime is how long data stays fresh before being considered stale. Fresh data won't refetch on mount. Set staleTime: Infinity for data that rarely changes, or lower values for frequently updated data.

Use queryClient.invalidateQueries({ queryKey: ['todos'] }) in the onSuccess callback of useMutation. This triggers a refetch of matching queries, updating the UI with fresh data.

Server Components can fetch data directly without TanStack Query. Use TanStack Query in Client Components for interactive data that needs caching, refetching, or real-time updates.

Related Posts