Most frontend tutorials show the perfect scenario:
- User opens the page
- Frontend calls the API
- API responds quickly
- Data appears
But real products don’t always work like that.
Sometimes an API takes 8 seconds.
Sometimes even longer.
And when that happens, the frontend has an important job: keep the user informed and maintain trust.
This is Day 1 of my series:
Frontend Problems Nobody Talks About
Let’s look at how I would think about a slow API response in a real application.
The Problem
Imagine a user opens a dashboard.
The frontend makes an API request:
js
const response = await fetch("/api/dashboard");
const data = await response.json();
The request takes 8 seconds.
What does the user see during those 8 seconds?
If the answer is:
- A blank screen
- A frozen interface
- An endless spinner
then the frontend experience is already failing.
The API may eventually succeed, but the user doesn’t know that.
They may think:
- Is the application broken?
- Did my click work?
- Should I refresh?
- Should I click again?
This is why slow APIs are not only backend problems.
They are also frontend UX problems.
1. Show a Skeleton UI
Instead of showing a blank page, display the structure of the content immediately.
For example:
function Dashboard() {
const { data, isLoading } = useDashboard();
if (isLoading) {
return <DashboardSkeleton />;
}
return <DashboardContent data={data} />;
}
A skeleton gives the user visual feedback that content is loading.
More importantly, it shows what kind of content they can expect.
2. Keep Previous Data Visible
Imagine a user changes a filter:
Last 7 Days → Last 30 Days
Should the entire dashboard disappear while new data loads?
Usually, no.
A better experience is:
- Keep the previous data visible
- Show that new data is being fetched
- Replace it when the request completes
With TanStack Query, this pattern becomes easier to manage.
const { data, isFetching } = useQuery({
queryKey: ["dashboard", filter],
queryFn: () => fetchDashboard(filter),
placeholderData: (previousData) => previousData,
});
Now the interface doesn’t suddenly become empty every time the query changes.
3. Distinguish Initial Loading from Background Fetching
These two states are different.
Initial Loading
The user has no data yet.
A skeleton UI makes sense.
Background Fetching
The user already has data, but the application is refreshing it.
In that case, removing the existing UI may create unnecessary disruption.
Instead, show subtle feedback:
{isFetching && <span>Updating...</span>}
This small distinction can significantly improve perceived performance.
4. Handle Errors Clearly
Slow requests can fail.
The frontend should never leave the user with an endless loading state.
Instead of:
Loading...
Loading...
Loading...
show something actionable:
function ErrorState({ retry }) {
return (
<div>
<p>We couldn't load your dashboard.</p>
<button onClick={retry}>Try Again</button>
</div>
);
}
A useful error state should answer two questions:
- What happened?
- What can I do next?
5. Think Carefully About Retries
Automatic retries can help with temporary network failures.
But retries should be intentional.
For example, retrying a dashboard data request may be reasonable.
Retrying a payment request blindly can be dangerous.
useQuery({
queryKey: ["dashboard"],
queryFn: fetchDashboard,
retry: 2,
});
The correct retry strategy depends on the operation.
The Bigger Lesson
Good frontend engineering is not only about rendering successful API responses.
It is about designing for:
- Slow networks
- Failed requests
- Empty responses
- Background updates
- Duplicate actions
- Unexpected states
The happy path is only one part of a real product.
A production-ready frontend should answer:
What does the user experience when things don’t go as planned?
That is where frontend engineering becomes much more interesting.
If you work on frontend applications, I’d love to hear how you handle slow API responses in production.
Top comments (0)