Today I implemented a small but significant UX improvement: empty-state feedback in my Book Tracker app. When a user applies a filter that returns zero results, they now see a clear message instead of an empty table that looks “broken” or “stuck.”
This change required no refactoring of core logic, no performance trade-offs, and actually made the code more readable by clarifying intent right where it matters.
The Before: Silent Failure on Empty Results
My BookTable.jsx component originally rendered rows like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<tbody> {readings.map((reading) => { const title = reading.book.title; const authors = reading.book.authors.join(“, “); const status = reading.status.charAt(0).toUpperCase() + reading.status.slice(1); const rating = reading.rating !== null ? `${reading.rating} / 5` : “—”; const notes = reading.notes || “”; return ( <tr key={reading.id}> <td>{title}</td> <td>{authors}</td> <td>{status}</td> <td>{rating}</td> <td>{notes}</td> </tr> ); })} </tbody> |
While this worked perfectly when data existed, it had a silent failure mode: when readings was an empty array, .map() returned nothing, leaving <tbody> completely empty.
From a user’s perspective:
-
The table header appears
-
But there’s no content beneath it
-
No explanation of why
-
It feels like the app might be broken or still loading
The After: Clear, Context-Aware Feedback
Here’s what I changed:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<tbody> {readings.length === 0 ? ( <tr> <td colSpan=“5” className=“no-results”> No books match this filter </td> </tr> ) : ( readings.map((reading) => { const title = reading.book.title; const authors = reading.book.authors.join(“, “); const status = reading.status.charAt(0).toUpperCase() + reading.status.slice(1); const rating = reading.rating !== null ? `${reading.rating} / 5` : “—”; const notes = reading.notes || “”; return ( <tr key={reading.id}> <td>{title}</td> <td>{authors}</td> <td>{status}</td> <td>{rating}</td> <td>{notes}</td> </tr> ); }) )} </tbody> |
Why This Matters
From a Developer’s Perspective:
-
Zero logic changes: The core table rendering remains untouched
-
Minimal code addition: Just a conditional wrapper around existing logic
-
Readability improved: The empty state is now explicit rather than implicit
-
No performance impact: The check is trivial (
readings.length)
From a User’s Perspective:
-
Clear feedback: “No books match this filter” explains exactly what’s happening
-
Increased trust: Transparency builds confidence in the app
-
Reduced frustration: No more wondering if something is broken
-
Thoughtful feel: The app anticipates and addresses their confusion
The Technical Details
-
colSpan="5": This ensures the message spans all five table columns, maintaining proper table structure -
Conditional logic: Shows only when
readings.length === 0, not during initial loading -
Context-aware: The message appears specifically after filtering, not just on initial load
-
Styling ready: The
no-resultsCSS class allows for subtle visual treatment (centered text, lighter color, etc.)
The Mental Model Behind This Change
When designing user feedback, I think through these layers:
-
When does this happen?
After filtering returns zero items -
What’s the user’s mental state?
“Why don’t I see anything?” -
What’s the actual data status?
Data has arrived, but there are no matches -
What’s the technical trigger?
readings.length === 0 -
How persistent is this state?
Can reappear every time the user changes the filter
This approach transforms what could be a confusing dead-end into a helpful, informative moment in the user’s journey.
Small Change, Professional Polish
Empty states are one of those subtle details that separate amateur interfaces from professional ones. They transform silent failures into communicative moments. The best part? This improvement cost me about 5 minutes of coding and made the app feel significantly more polished.
What’s your approach to handling empty states? Do you prefer messages, illustrations, or suggested actions? Share your thoughts below.
This is another entry in my “Diary of a Developer” series, where I document real coding improvements that bridge the gap between functional and thoughtful. More to come!




Leave a Reply