The Catalogue List & Detail Views
Personal Catalogue: React, Express & MongoDB
Chapter 7 · The Catalogue List & Detail Views
Every piece needed to actually see the catalogue already exists — Chapter 4's AddItemForm,
Chapter 5's TagEditor and TagFilter, Chapter 6's SearchBar, and Chapter
3's list/detail routes underneath all of them. Nothing has rendered an actual item on screen yet. This
chapter builds the two views that finally do: a compact list across every type, and a detail view that shows
a specific item's own full, real shape.
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 — And a Real Rendering Bug
Every item type needs its own "creator" shown somewhere in the list — a Book's author, a Cd's artist, a Dvd's or Bluray's director. A first, reasonable-looking attempt reaches for a template literal:
For a Book, this looks completely correct — item.author holds a real name, and the template
literal produces exactly the string expected. For a Cd, a Dvd, or a Bluray, item.author doesn't
exist at all; it's undefined. JavaScript's own template-literal interpolation coerces that with
String(undefined), which returns the literal four-character string "undefined" —
and because that's now a genuine string value, React renders it exactly as given. Every non-Book item in the
list ends up visibly showing "By undefined" on screen.
<p>By {'{item.author}'}</p> instead — with the value interpolated directly
as a separate JSX child, not inside a template literal string — wouldn't produce the literal word
"undefined." React treats undefined as an empty, invisible child when it appears directly in
JSX. But it isn't really fixed either: the visible result for a non-Book item becomes a stray, orphaned
"By" label with no name after it — a smaller bug, but still a real one.
The Real Fix: A getCreator() Helper and Conditional Rendering
The actual fix needs to know whether a creator exists at all, not just avoid coercing a missing one into text:
getCreator(item) genuinely returns null when none of the three creator fields
exists — not the string "null", an actual null value. null && <p>...</p>
short-circuits to null without ever evaluating the JSX on the right, and React renders nothing
for it — the "By" line simply doesn't appear at all for a Cd, Dvd, or Bluray, which is the actually correct
behavior, not just a less-broken one.
The Item Detail Component: Reusing Chapter 4's FIELD_CONFIG
The detail view needs to know exactly which fields a given item's own type has — the same information
Chapter 4's FIELD_CONFIG already encodes for the add-item form. Importing it here instead of
writing a second, separate list means the two can never quietly drift apart:
tags is deliberately excluded from the generic field loop and rendered by the real
TagEditor instead — every other array field (Cd's own tracklist, say) still falls
through the same loop and is joined into a plain comma-separated line, since only tags need to actually be
editable here.
TagEditor's onChange prop has existed since Chapter 5, calling
onChange?.(updated.tags) after every real add or remove — but nothing ever passed a real
function into it until now. Wiring it to ItemDetail's own onTagsChange prop closes
that loop: editing a book's tags here now propagates back up to the shared list, resolving the exact
stale-data problem this chapter would otherwise have — going back to the list after a tag edit and seeing
the book still showing its old tags, because two separate copies of the same document had drifted apart.
GET /api/items/:id at all — it just looks up the matching item
already sitting in the parent's own items array. That's only safe because Chapter 3's list route
uses .lean(), which returns every field a document actually has, not just the ones on
Item's own base schema. Without that fix, the list's own copy of a Book would be missing
author and tags entirely, and this shortcut wouldn't work.
Wiring State in App.jsx
One shared items array, lifted into App, is what lets every other component —
the form, the tag filter, the search bar, the detail view — affect the exact same list rather than each
holding its own disconnected copy:
Search and tag filtering both call setItems directly, replacing the whole displayed list with
whatever the server just returned — exactly matching how Chapter 5's TagFilter and Chapter 6's
SearchBar were already built, with no changes needed to either. Creating a new item appends to
the existing array instead of replacing it, since a fresh item shouldn't make an active search or tag filter
disappear.
Trying It in the Browser
With both servers running, add one item of each type through the form and watch the list itself: every
entry shows its own correct creator with no stray "By undefined" text anywhere, and clicking any item opens
its own detail view with exactly the fields that type actually has. Open a Book, add a tag through the real
TagEditor, then click "Back to list" — the book's own list entry doesn't show tags directly, but
reopening its detail view immediately reflects the change, confirming the shared items array
genuinely updated rather than the edit only existing inside the now-closed detail view.
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 revert ItemList's summary line to the naive template-literal version (`By ${item.author}`) and confirm the real "By undefined" text appears for a Cd, Dvd, or Bluray. Restore the getCreator() version and confirm the fix.
📄 View solutionTemporarily remove the onTagsChange wiring between ItemDetail and App (pass no onChange to TagEditor at all), then edit a book's tags, go back to the list, and reopen it — confirm the stale-tags bug reappears. Restore the wiring and confirm the fix.
📄 View solutionChapter 7 Quick Reference
- getCreator(item) — returns whichever of author/artist/director exists, or a real null, driving genuinely conditional rendering rather than a coerced "undefined" string
- Template literals vs. JSX children — string interpolation coerces a missing value into the literal text "undefined"; a plain JSX child silently renders nothing instead, but still needs a real presence check for a correct result
- FIELD_CONFIG reused — the exact same object from Chapter 4 drives both which inputs the form shows and which fields the detail view displays
- No extra fetch on detail — a direct, explained payoff of Chapter 3's own .lean() fix; every field is already present in the shared list
- TagEditor's onChange, finally wired — closes the loop Chapter 5 left open, keeping the list and detail view's own copies of tags in sync
- Two filter controls, not one — SearchBar and TagFilter stay separate since they genuinely query different things
- Next chapter: Fast Manual Entry — reducing friction in the add-item flow given the project's own real deadline