Why Your App Feels Broken: The Art of Loading and Error States
0. Why Speed is Money (Pre-reading)
Before diving into code, let's talk about why we should obsess over loading states.
There is a famous study by Amazon.
"Every 100ms latency costs 1% in sales."
Conversely, designing a good loading screen can significantly reduce bounce rates.
Reducing physical API latency is hard (physics), but reducing psychological wait time is possible through design.
1. "Why Does Your App Flicker?"
That's what my friend said when he saw the early version of my app.
"It works, but... it feels cheap. Every time I click, the screen flashes and content jumps around."
The cause was the so-called 'Layout Shift'.
I showed nothing (return null) while fetching data, then suddenly rendered the screen when data arrived.
/* Bad Example */
if (loading) return null; // White screen during loading
if (error) return <div>Error!</div>;
return <Content data={data} />;
This 0.5-second white screen made users anxious ("Did it freeze?"),
and the sudden content pop-in caused eye strain.
1.1. Why Google Hates CLS (Cumulative Layout Shift)
Have you ever tried to click a 'Cancel' button, but an ad popped up and you accidentally clicked 'Buy'?
Google calls this CLS (Cumulative Layout Shift).
It's a major factor in Core Web Vitals that kills your SEO score.
Google considers this "Deceptive UI", not just "Ugly UI".
It happens when images without defined height push content down after loading.
2. Designing the Wait: Skeleton UI
Open YouTube or Facebook. Instead of a spinning circle, you see gray boxes (bones) first.
This is Skeleton UI.
I adopted react-loading-skeleton and replaced all loading screens.
/* Good Example */
if (loading) {
return (
<div className="card">
<Skeleton height={200} /> {/* Image placeholder */}
<Skeleton count={3} /> {/* 3 lines of text placeholder */}
</div>
);
}
The result was amazing.
The API speed was the same, but users felt "The app became much faster."
Because the brain anticipates "Ah, an image will appear there," the wait feels shorter.
Lesson: Performance optimization includes increasing 'Perceived Performance' too.
2.1. Rules for a Perfect Skeleton
Not all skeletons are created equal. Here are 3 rules:
- Match the Height: If skeleton height != content height, you get Layout Shift (CLS) when loaded.
- Subtle Animation: Without a pulse or wave animation, the app looks frozen.
- Don't Show Too Long: If a skeleton persists for 10+ seconds, users get frustrated. In that case, switch to a text message like "Taking longer than expected...".
3. "There's an Error?" (The End of Alert Boxes)
A bigger problem was error handling.
When APIs failed, I lazily did this:
.catch(err => alert("Server Error Occurred."))
This is the worst for UX.
A warning pops up, and when they click OK, the screen stays white or unresponsive.
"So what? Should I refresh? Reinstall the app?"
I introduced Error Bundle and Graceful Degradation.
Partial Error Handling
If one photo in an Instagram feed fails to load, the whole app shouldn't crash.
Show a "Failed to load image [Retry]" button for that part only, and show the rest normally.
/* The Majesty of React Query */
if (isError) {
return (
<div className="error-box">
<p>Failed to load data.</p>
<button onClick={() => refetch()}>Retry</button>
</div>
);
}
Isolating the "Failed Part" from the "Alive Part" stopped users from thinking the app was 'broken'.
3.1. Technical Deep Dive: React Error Boundary
Standard try-catch often misses errors in async code or event handlers because they run outside React's render lifecycle.
I highly recommend react-error-boundary.
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset state on retry (e.g., refetch query)
resetQueries();
}}
>
<MyComponent />
</ErrorBoundary>
);
}
It allows Declarative Error Handling, making your code much cleaner than scattered try-catch blocks.
4. Preventing Rage Clicks (Careful with Optimistic UI)
Ever clicked a "Like" button multiple times because it was slow?
We usually disabled the button during loading to prevent this.
But a better way is Optimistic UI.
Believing "The server will succeed," we update the UI number by +1 immediately.
- User Clicks
- UI: Heart turns red (+1)
- Background: Send API Request
- (If fails?) -> Quietly rollback and show a toast message
This gives instant responsiveness, like the app is glued to your finger.
(Of course, don't use this for payments. Double billing is a disaster.)
4.1. Declarative Loading: React Suspense
With React 18, defensive code like return null or if (loading) is becoming obsolete.
<Suspense> automatically detects loading states and shows the skeleton.
/* Before: Imperative */
if (loading) return <Skeleton />;
return <Data />;
/* After: Declarative */
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
This is the future of modern frontend. Focusing on "What" to show, not "How" to load it.
5. Conclusion: White Screens Are a Sin
Developers spend less time staring at loading bars than users. (Localhost is fast.)
But users use our apps on unstable Wi-Fi and slow LTE.
- No White Screens (Use Skeletons)
- No alert() (Show pretty error components)
- Provide Retry Buttons (Give control to users)
Just following these three rules will make your app look less like "Homework" and more like a "Product".
Even the waiting time is part of your service.
5.1. The Psychology of Waiting (Maister's Law)
According to David Maister's "The Psychology of Waiting Lines":
-
Unoccupied time feels longer than occupied time.
This is why Skeleton UI works. It gives the eye something to scan, "occupying" the user's attention. A white screen is "unoccupied time".
-
Unexplained waits are longer than explained waits.
If a spinner spins for 10 seconds without text, users panic. "Is it broken?"
Adding a text "Creating your report..." explains the wait and reduces abandonment.
-
Anxiety makes waits seem longer.
The uncertainty of "Did my click register?" causes anxiety.
Immediate feedback (Ripple effect, Active state) removes this anxiety instantly.
🎁 Bonus: The "Polished App" Checklist
- Font Loading Check: Did you use
font-display: swap to prevent invisible text (FOIT)?
- Image Lazy Loading: Are off-screen images loaded lazily? (
loading="lazy")
- Active State Feedback: Does the button change color or show a ripple when clicked?
- 404 Page Design: Did you give lost users a "Go Home" button?
- Toast Messages: Are you elegantly notifying success/failure states?
Tick these boxes, and your app is already in the top 1%.