Why Senior Developers Choose Tanstack Query Over Fetch and useEffect

You've just finished building your first React component that fetches data. You used the trusty combination of fetch and useEffect - it works, but something feels off. Maybe you've noticed your app re-fetching data unnecessarily, or you're drowning in loading states and error handling code. If this sounds familiar, you're not alone.

As a senior developer who's been through these trenches, let me explain why libraries like Tanstack Query (formerly React Query) have become the go-to solution for data fetching in modern React applications.

The Problem with Traditional Data Fetching

Let's look at a typical data fetching setup using fetch and useEffect:

function UserProfile({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return null;

  return <div>{data.name}</div>;
}

This code might look familiar. It works, but it comes with several hidden problems:

  1. You're managing three pieces of state (data, loading, error) for every data fetch

  2. There's no built-in caching - if the component remounts, it fetches again

  3. If the user navigates away and back, they see a loading state again

  4. No automatic background updates or refetching on window focus

  5. No way to know if the data is stale

  6. Error handling and retries need to be implemented manually

The React team has even noted that while useEffect can be used for data fetching, it's not the ideal solution. According to the React documentation, useEffect is primarily meant for synchronization with external systems, not data fetching.

Enter Tanstack Query

Here's how the same component looks with Tanstack Query:

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
  });

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

  return <div>{data.name}</div>;
}

Not only is this code more concise, but it comes with powerful features out of the box:

1. Automatic Caching and Background Updates

Tanstack Query maintains a cache of your data and automatically manages its lifecycle. When you request the same data again, it:

  • Returns cached data immediately

  • Fetches fresh data in the background

  • Updates the UI automatically when new data arrives

  • Allows you to configure how and when data should be considered stale

2. Smart Request Deduplication

If multiple components request the same data simultaneously, Tanstack Query will only make one network request and share the result with all components. This is particularly valuable in larger applications where the same data might be needed in multiple places.

3. Built-in Loading and Error States

Instead of managing multiple pieces of state manually, Tanstack Query provides a unified interface for handling loading, error, and success states:

const { data, isLoading, isError, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

4. Automatic Retries and Error Handling

When a request fails, Tanstack Query will automatically retry it with exponential backoff, and you can configure the retry behavior to match your needs:

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3, // Will retry failed requests 3 times
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

Advanced Features That Make Life Easier

Optimistic Updates

One of the most powerful features of Tanstack Query is optimistic updates. Instead of waiting for the server response, you can update the UI immediately while the mutation is in progress:

const queryClient = useQueryClient();

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

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

    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], old => [...old, newTodo]);

    // Return a context object with the snapshotted value
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // If the mutation fails, use the context returned from onMutate to roll back
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
});

Prefetching and Parallel Queries

Tanstack Query makes it easy to prefetch data before it's needed:

// Prefetch data for the next page
const prefetchNextPage = async () => {
  await queryClient.prefetchQuery({
    queryKey: ['todos', page + 1],
    queryFn: () => fetchTodoPage(page + 1),
  });
};

// Run multiple queries in parallel
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });

Infinite Queries for Pagination

Implementing infinite scroll becomes trivial:

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjectPage,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});

When to Make the Switch

You should consider switching to Tanstack Query when:

  1. You find yourself writing the same data-fetching boilerplate code repeatedly

  2. Your application needs real-time updates or background refetching

  3. You want better control over caching and invalidation

  4. You need advanced features like optimistic updates or infinite scrolling

  5. You're building a larger application where data management becomes complex

Integration with Modern React Frameworks

Tanstack Query works seamlessly with modern React frameworks like Next.js. While Next.js provides its own data fetching methods, Tanstack Query can complement these features, especially for client-side data management:

// pages/users/[id].js
export default function UserPage({ initialData }) {
  const { data } = useQuery({
    queryKey: ['user', id],
    queryFn: fetchUser,
    initialData, // Hydrate from SSR data
  });

  return <UserProfile user={data} />;
}

// Get initial data during SSR
export async function getServerSideProps({ params }) {
  const queryClient = new QueryClient();
  
  await queryClient.prefetchQuery({
    queryKey: ['user', params.id],
    queryFn: () => fetchUser(params.id),
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}

Conclusion

While fetch and useEffect might seem simpler at first, they quickly become unwieldy as your application grows. Tanstack Query provides a robust solution that handles the complexities of data fetching, caching, and state management, allowing you to focus on building features rather than managing data-fetching logic.

The initial learning curve is worth the investment, as it will save you countless hours of debugging and implementing features that come out of the box with Tanstack Query. As your application grows, you'll appreciate having a battle-tested solution that handles the intricacies of data management for you.

Remember, tools like Tanstack Query aren't just about writing less code - they're about writing more maintainable, performant, and user-friendly applications. The next time you reach for fetch and useEffect, consider whether Tanstack Query might be a better fit for your needs.

Raymond Yeh

Raymond Yeh

Published on 12 December 2024

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
Should I Use TanStack Query When Most of My Components Are Server Components?

Should I Use TanStack Query When Most of My Components Are Server Components?

Learn why TanStack Query remains crucial in a server-component-heavy architecture and see its impact on UI performance and developer efficiency.

Read Full Story
SSR with Remix using TanStack Query

SSR with Remix using TanStack Query

Explore SSR with Remix and TanStack Query in this article, focusing on setup, best practices, and real-world examples to boost SEO, enhance load times, and ensure smoother user interactions.

Read Full Story
When to Say No to Next.js: The Guide for Minimalist Web Developers

When to Say No to Next.js: The Guide for Minimalist Web Developers

The appeal of Next.js is obvious - built-in server-side rendering, static site generation, file-based routing, and API routes promise a smooth, full-stack development experience. But at what cost?

Read Full Story