The Catalogue List & Detail Views in React
Personal Catalogue: React & Firebase
Chapter 7 · The Catalogue List & Detail Views in React
Every piece needed to actually see the catalogue already exists — Chapter 4's AddItemForm,
Chapter 5's tag operations, Chapter 6's searchItems(). Nothing has rendered an actual item on
screen yet. This chapter builds the two views that finally do — and finds that Firestore's own real-time
capabilities quietly remove a whole category of manual wiring the MongoDB sibling's own equivalent chapter had
to build by hand.
Two Views, Two Real Responsibilities
| Aspect | List View | Detail View |
|---|---|---|
| Data shown | Title, type, creator, release year — a compact summary | Every field the item's own type actually has, via Chapter 4's FIELD_CONFIG |
| Tag editing | Not available | The full TagEditor from Chapter 5, Book only |
| Navigation | Click an item to open its detail view | "Back to list" returns |
| Data source | The shared items array in App state | The same shared array — no extra fetch |
A Naive Summary Card — the Same Rendering Bug, Backend-Agnostic
Every item type needs its own "creator" shown somewhere in the list. This particular bug has nothing to do with which database is behind it — it's a plain JavaScript coercion problem the MongoDB sibling's own Chapter 7 already found, and it would bite this course exactly as hard if it weren't caught here too:
For a Cd, Dvd, or Bluray, item.author is undefined — the template literal coerces it
with String(undefined), producing the literal text "By undefined", which React
then renders exactly as given. The fix is the same real one, unchanged from the sibling course:
getCreator(item) returns a real null when no creator field exists, and
null && <p>...</p> short-circuits without rendering anything at all — the
correct outcome, not just a less-broken one.
The Item Detail Component: Reusing Chapter 4's FIELD_CONFIG
GET /api/items/:id call only because its
list route uses .lean(), returning every field a document has rather than only the base
Item schema's own fields. This course never needed an equivalent fix at all: Chapter 2 already
established that a Firestore document fetched any way at all always carries every field it was written with —
there was never a base-model-vs-subtype-model split here for a "lean" option to work around in the first
place.
The Naive Data Flow: One Fetch, Then Drift
A first, reasonable approach loads the catalogue once on mount, the same shape every prior chapter's own standalone examples have used:
Open a Book's detail view, add a tag through TagEditor, click "Back to list," then reopen the
same book: its detail view shows the old tags again. The write to Firestore genuinely succeeded — the
document itself is correct — but the items array sitting in App's own React state was
only ever populated once, at mount, and nothing told it a document underneath it had changed since. This is
exactly the stale-data gap the MongoDB sibling's own Chapter 5 onChange prop exists to close.
The Real Fix: a Live Firestore Listener
Firestore's client SDK offers something the MongoDB sibling's own REST API has no equivalent of at all: a subscription that keeps delivering fresh data automatically, for as long as it stays open, with no manual "please refetch" call needed anywhere in the app.
onSnapshot() fires immediately with the collection's own current state, exactly like the naive
getDocs() version — but then fires again, automatically, every single time any document
in the items collection changes for any reason: a new item created by AddItemForm, a
tag added or removed by TagEditor, anything at all. setItems() is called with a
genuinely fresh snapshot every time, with nothing in any other component needing to know this is happening.
TagEditor's onChange prop as the
real fix for the stale-list problem — a genuine, necessary piece of manual state-lifting. Here, the exact same
stale-list problem is already solved the moment onSnapshot() replaces the one-time fetch, with no
prop wiring required at all: the tag write itself is what triggers a fresh snapshot, and the fresh snapshot is
what refreshes items, and selectedItem (derived fresh from items on
every render) reflects the change automatically. Chapter 5's own onChange prop still exists and
still works, but it's no longer the thing actually keeping the list and detail view in sync — that job now
belongs to the listener, not to any one component remembering to call a callback.
The same mechanism quietly removes a second piece of manual wiring, too: AddItemForm's own
onCreated callback, previously needed to append a freshly created item into the shared array by
hand, is no longer required for the list to show a new item — the listener already picks it up the moment
addDoc() succeeds.
unsubscribe() function when a component unmounts
leaves the listener running in the background indefinitely, a real, avoidable source of both wasted reads and
a memory leak.
Wiring State in App.jsx
items is now the live, listener-driven source of truth; filtered holds whatever
SearchBar or TagFilter most recently returned, and clearing it after a new item is
created is enough to bring the full, current list back into view — since items itself is already
correct the instant the listener's own next snapshot arrives.
TagFilter is a thin wrapper around Chapter 5's own findBooksByTag() function, exactly
the way SearchBar already wraps Chapter 6's searchItems():
array-contains
query to Firestore. Merging them into one input would hide that real difference rather than resolve it.
Trying It in the Browser
Add one item of each type through the form and watch the list update itself with no reload — the listener
already has it. Open a Book, add a tag through the real TagEditor, click "Back to list," then
reopen the same book: the new tag is already there, with no onChange prop, no manual state
update, and no page refresh involved anywhere in making that happen.
Hands-On Exercises
Build ItemList, ItemDetail, and the updated App.jsx exactly as this chapter describes, then create one item of each type and confirm every list entry shows a correct creator line (or none at all for a type with no creator field set) with no "By undefined" text anywhere.
📄 View solutionTemporarily replace the onSnapshot listener in App.jsx with the earlier one-time getDocs() fetch, reproduce the stale-tags bug (edit a book's tags, go back, reopen it), and confirm the old tags are shown. Restore the listener and confirm the fix — with no other code changed anywhere.
📄 View solutionExplain precisely why switching to onSnapshot() fixes the stale-tags bug without needing any change to TagEditor, ItemDetail, or AddItemForm — and name the one real, ongoing cost that switch introduces which a one-time getDocs() call never had.
📄 View solutionChapter 7 Quick Reference
- getCreator(item) — the same backend-agnostic "By undefined" rendering bug the MongoDB sibling found, fixed the identical way
- FIELD_CONFIG reused — the same object from Chapter 4 drives both the add-item form and the detail view
- No extra fetch on detail — true here for an even more fundamental reason than the MongoDB sibling's .lean() fix: every Firestore document always carries every field it was written with
- onSnapshot() — a live listener replacing a one-time fetch; fires on mount and again on every future change, anywhere
- Manual sync wiring eliminated — Chapter 5's onChange prop and AddItemForm's onCreated callback are both no longer required to keep the list and detail view in sync
- Not free — a live listener still counts real Firestore reads and must be unsubscribed on unmount
- Next chapter: Fast Manual Entry — reducing friction in the add-item flow given the project's own real deadline