For years, React data fetching looked like a succession of useEffect, useState, isLoading, and error conditions. It works for a small page. As an application grows, the approach becomes harder to maintain.
That is where TanStack Query and React Suspense significantly improve the workflow.
Server state vs local state
Data fetched from an API is not simply React state.
It has:
- a lifecycle;
- a cache;
- a fetch timestamp;
- a freshness state;
- errors;
- potentially multiple consumers.
TanStack Query is designed around exactly this problem.
const { data, isPending, error } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
});
The queryKey identifies the resource and lets the library manage its cache.
The cache changes everything
Imagine an application with:
Dashboard
├── Users
├── Orders
└── Statistics
Multiple components may depend on the same data. Instead of each one issuing its own request, TanStack Query can reuse cached data and manage refetching.
This reduces code and makes transitions more predictable.
Suspense for loading states
With React Suspense, components do not need to turn every loading state into a collection of conditions.
<Suspense fallback={<UsersSkeleton />}>
<UsersList />
</Suspense>
The skeleton becomes the responsibility of the UI boundary.
Invalidating after mutations
After creating or updating data, I prefer explicitly invalidating affected resources:
await queryClient.invalidateQueries({
queryKey: ["users"],
});
The UI can then obtain fresh data without every component knowing the details of the request.
What I keep in local state
Not everything belongs in TanStack Query.
I generally reserve local state for:
- modal visibility;
- selected tabs;
- temporary input;
- UI preferences;
- purely visual state.
Server-provided data stays in the server cache.
A cleaner architecture
React UI
│
├── Local state
│
└── TanStack Query
│
▼
API
│
▼
Backend
This separation makes it easy to understand where each piece of information belongs.
Performance
Caching is not only an optimization. It also avoids unnecessary requests and allows background revalidation.
Depending on the use case, I configure:
staleTime;gcTime;- retries;
- pagination;
- prefetching.
There is no universal configuration. Nearly immutable data should not have the same strategy as a live order dashboard.
My principle
I do not want every component to own the entire network lifecycle.
TanStack Query manages server data. Suspense manages UI transitions. Components focus on rendering.
