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:
- Create a
useStatefordata. - Create a
useStateforisLoading. - Create a
useStateforerror. - Write a
useEffectto fetch the data. - Realize we forgot to handle race conditions like what if the component unmounts before the fetch finishes?
- 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
stalewithout 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>;
}
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>;
}
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
staleTimeis5000(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
UserProfilepage, 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'] });
},
});
When this runs:
- The mutation succeeds.
- React Query marks the
['user']cache as Stale immediately. - 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)