DEV Community

Cover image for TanStack Query Changed Everything (Part 3)
Peter Ogbonna
Peter Ogbonna

Posted on

TanStack Query Changed Everything (Part 3)

In Part 2, we learnt about the invisible layer of HTTP caching. Now, we move to the application layer.

You can relate to the nightmare associated with managing data coming from the server.

We have all written this code before:

  1. Create a useState for data.
  2. Create a useState for isLoading.
  3. Create a useState for error.
  4. Write a useEffect to fetch the data.
  5. Realize we forgot to handle race conditions like what if the component unmounts before the fetch finishes?
  6. Realize we have no way to deduplicate requests if two components need the same data.

As I argued in my previous article about React Router Loaders, manual state management looks messy and at times, tend to be where bugs go to hide.

Today, we look at TanStack Query (React Query)*. It isn't just a data fetching library; it is an async state manager that brings the stale-while-revalidate strategy (discussed in part 2) directly into your components.

The Philosophy: Server State vs. Client State

The biggest breakthrough TanStack Query brought to the frontend world is the realization that Server State is not the same as Client State.

  • Client State asks these questions: Is the modal open? What is the current theme? Is the sidebar collapsed? All these belongs in useState, Context, or Zustand).

  • For Server State, we talk about: The list of users, the current blog post.

Server state is "borrowed" as it's data that exists remotely, owned by the server (somebody else), hence can become stale without you knowing.

TanStack Query is designed explicitly to manage server state.

A Side-by-Side Comparison

Let's look at the difference.

The Old Way - using useEffect:

function UserProfile() {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) {
            setUser(data);
            setIsLoading(false);
        }
      })
      .catch((err) => {
         if (isMounted) setError(err);
      });

    return () => { isMounted = false };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The New way - using TanStack Query:

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

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

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

It looks similar, but under the hood, TanStack Query is doing something magical, which is Global Caching.

If you use this <UserProfile /> component in 5 different places on your screen, React Query will deduplicate the requests. It will only fetch once and serve the result to all 5 components instantly.

The Configuration: staleTime vs gcTime

TanStack Query uses the combo of these two to handle caching

1. staleTime (The "Freshness" Timer)

This controls when data is considered old.

Default is 0 (Zero).

  • If staleTime is 5000 (5 seconds), the data remains Fresh for 5 seconds. During this time, if another component asks for this data, it uses the cache preventing a background refetch.

  • After 5 seconds, the data becomes Stale, TanStack Query will still serve it instantly but will trigger a background refetch to update it.

It's default is zero, because TanStack Query assumes your data is always out of date.

2. gcTime (The "Garbage Collection" Timer)

Formerly known as cacheTime.

This controls how long unused data stays in memory.

Default is 300000 (5 minutes).

  • If you navigate away from the UserProfile page, the data is no longer being used.

  • React Query keeps it in memory for 5 minutes in case you come back.

  • If you return within 5 minutes, the data appears instantly else, the data is gone and you get a refetch.

Solving the Hardest Problem (Invalidation)

In Part 1, we said invalidation is the hardest problem in CS.

TanStack Query solves this with invalidateQueries.

Imagine you have a mutation to update a user's name.

const mutation = useMutation({
  mutationFn: updateUser,
  onSuccess: () => {
    // This tells TanStack Query the 'user' data is now dirty. It uses the 'queryKey' to identify which to invalidate
    queryClient.invalidateQueries({ queryKey: ['user'] });
  },
});
Enter fullscreen mode Exit fullscreen mode

When this runs:

  1. The mutation succeeds.
  2. React Query marks the ['user'] cache as Stale immediately.
  3. Any component currently displaying the user triggers a background refetch, hence automatically updating the UI!

What’s Next?

TanStack Query is the standard for general Frontend applications. But what if you are using Redux? What if you want this caching power but integrated tightly with your global store?

In Part 4, we will explore RTK Query (Redux Toolkit Query). We will see how it takes these same concepts and applies them to the Redux ecosystem.

See you in Part 4.

Top comments (0)