TechnologyFebruary 5, 2026• 8 min read|
...
...

Mastering Data Fetching with TanStack Query: A Modern Approach

Vantol Bennett

By Vantol Bennett

Author

Mastering Data Fetching with TanStack Query: A Modern Approach

Introduction

Data fetching is one of the most common challenges developers face when building modern web applications. While it might seem straightforward at first, managing server state quickly becomes complex when you need to handle caching, background updates, pagination, and error states. This is where TanStack Query (formerly React Query) comes in—a powerful library that transforms how we think about server state management.

What is TanStack Query?

TanStack Query is a data-fetching and state management library that provides powerful tools for fetching, caching, synchronizing, and updating server state in your applications. Unlike traditional state management solutions, TanStack Query treats server data as a first-class citizen, recognizing that it has fundamentally different characteristics than client state.

The library works seamlessly with React, Vue, Svelte, and Solid, making it a versatile choice regardless of your framework preference. It eliminates the need for writing complex boilerplate code and helps you build more maintainable applications.

Why Use TanStack Query?#

Traditional data fetching approaches often require you to manage loading states, error handling, caching logic, and refetching strategies manually. TanStack Query handles all of this out of the box, providing:

  • Automatic caching with intelligent cache invalidation
  • Background refetching to keep data fresh
  • Optimistic updates for better UX
  • Request deduplication to prevent unnecessary network calls
  • Pagination and infinite scroll support
  • Prefetching for improved perceived performance

TanStack Query doesn't replace fetch or axios—it enhances them by providing a smart layer of abstraction for managing server state.

Getting Started

Let's dive into how you can start using TanStack Query in your React application.

Installation#

terminal
npm install @tanstack/react-query
# or
yarn add @tanstack/react-query

Basic Setup#

First, wrap your application with the QueryClientProvider:

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

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

Your First Query#

Here's a simple example of fetching user data:

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

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
}

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

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

🔑 Query Keys

Query keys are crucial in TanStack Query. They uniquely identify your queries and are used for caching, refetching, and cache invalidation. Use arrays with descriptive values for better organization.

Advanced Features

Mutations#

Mutations handle data updates, creations, and deletions. Here's how to create a mutation for updating user data:

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

interface UpdateUserData {
  name: string;
  email: string;
}

function UpdateUserForm({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (userData: UpdateUserData) =>
      fetch(`/api/users/${userId}`, {
        method: 'PUT',
        body: JSON.stringify(userData),
        headers: { 'Content-Type': 'application/json' },
      }),
    onSuccess: () => {
      // Invalidate and refetch user data
      queryClient.invalidateQueries({ queryKey: ['user', userId] });
    },
  });

  const handleSubmit = (data: UpdateUserData) => {
    mutation.mutate(data);
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      // Form handling logic
    }}>
      {mutation.isPending && <p>Updating...</p>}
      {mutation.isError && <p>Error updating user</p>}
      {mutation.isSuccess && <p>User updated successfully!</p>}
      {/* Form fields */}
    </form>
  );
}

Optimistic Updates#

Optimistic updates provide instant feedback by updating the UI before the server responds:

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

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

    // Optimistically update
    queryClient.setQueryData(['todos'], (old: Todo[]) => [...old, newTodo]);

    // Return context with the snapshot
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos'], context?.previousTodos);
  },
  onSettled: () => {
    // Refetch after error or success
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Performance Benefits#

Optimistic updates can make your application feel up to 10x faster by eliminating the perceived latency of server responses.

Pagination#

TanStack Query makes pagination simple with built-in support:

PaginatedList.tsx
function PaginatedPosts() {
  const [page, setPage] = useState(1);

  const { data, isLoading, isPlaceholderData } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    placeholderData: (previousData) => previousData,
  });

  return (
    <div>
      {data?.posts.map(post => <PostCard key={post.id} post={post} />)}
      
      <button
        onClick={() => setPage(old => Math.max(old - 1, 1))}
        disabled={page === 1}
      >
        Previous
      </button>
      
      <button
        onClick={() => setPage(old => old + 1)}
        disabled={isPlaceholderData || !data?.hasMore}
      >
        Next
      </button>
    </div>
  );
}

Best Practices

Always provide appropriate staleTime and gcTime values based on your data's characteristics. Not all data needs real-time updates!

Configure Cache Times Appropriately#

queryClient.ts
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
      retry: 3,
      refetchOnWindowFocus: true,
    },
  },
});

Use Query Keys Consistently#

Establish a naming convention for your query keys:

queryKeys.ts
export const queryKeys = {
  users: {
    all: ['users'] as const,
    detail: (id: string) => ['users', id] as const,
    posts: (id: string) => ['users', id, 'posts'] as const,
  },
  posts: {
    all: ['posts'] as const,
    detail: (id: string) => ['posts', id] as const,
  },
};

Enable DevTools#

TanStack Query comes with excellent developer tools:

App.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

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

🛠️ DevTools Pro Tip

The DevTools panel provides real-time insights into your queries, their states, and cached data. It's invaluable for debugging and understanding how TanStack Query manages your data.

Common Patterns

Dependent Queries#

Sometimes you need to fetch data based on previous query results:

DependentQueries.tsx
function UserPosts({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

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

  return <PostsList posts={posts} />;
}

Prefetching#

Improve perceived performance by prefetching data:

Prefetching.tsx
function PostList() {
  const queryClient = useQueryClient();

  const { data: posts } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  const handleMouseEnter = (postId: string) => {
    queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId),
    });
  };

  return (
    <div>
      {posts?.map(post => (
        <div 
          key={post.id}
          onMouseEnter={() => handleMouseEnter(post.id)}
        >
          <Link to={`/posts/${post.id}`}>{post.title}</Link>
        </div>
      ))}
    </div>
  );
}

Conclusion

TanStack Query has fundamentally changed how developers approach data fetching and state management in modern applications. By providing intelligent caching, automatic background updates, and a simple yet powerful API, it eliminates much of the boilerplate code traditionally associated with server state management.

Whether you're building a small application or a large-scale enterprise system, TanStack Query's flexibility and performance optimizations make it an excellent choice. The library's active community, comprehensive documentation, and framework-agnostic approach ensure it will remain a valuable tool in your development arsenal.

Start small with basic queries and gradually adopt advanced features like optimistic updates and prefetching as your application grows. Your users will appreciate the snappier experience, and your team will benefit from cleaner, more maintainable code.

Happy querying!

Stay Updated

Get the latest posts delivered straight to your inbox.

Comments

(0)