The Add-Item Form: Type-Specific Fields, Client-Side Validation
Personal Catalogue: React & Firebase
Chapter 4 · The Add-Item Form: Type-Specific Fields, Client-Side Validation
Chapter 3 finished the plumbing: a validateItem() call, then a direct Firestore write, with real
Security Rules standing behind both. This chapter builds the one form that actually produces the data going
into that pipeline — and finds two places where skipping a step that was purely cosmetic for the MongoDB
sibling would leave this app with a genuinely broken document.
The Same Field Configuration, Reused Directly
Personal Catalogue (React, Express & MongoDB)'s own Chapter 4 built a declarative
FIELD_CONFIG object, matched field-for-field against its Mongoose discriminator schemas, to drive
one shared rendering loop instead of four separate forms. Nothing about that idea is MongoDB-specific — it's
reused here verbatim, matched instead against this course's own Chapter 2 field list:
| Type | Firestore field (Ch.2) | Frontend config field |
|---|---|---|
| Book | author (required) | author, marked required |
isbn | isbn | |
publisher | publisher | |
tags (array) | tags, comma-separated text → array | |
| Cd | artist (required) | artist, marked required |
tracklist (array) | tracklist, comma-separated text → array | |
| Dvd | director | director |
runtimeMinutes (number) | runtimeMinutes, numeric input | |
regionCode | regionCode | |
| Bluray | director | director |
runtimeMinutes (number) | runtimeMinutes, numeric input | |
resolution ('1080p'/'4K') | resolution, a select with those two options |
Shared State, Type-Specific State
title, releaseYear, and notes are the base fields every item type
shares, so they get their own piece of state, kept separate from whichever type-specific fields the
FIELD_CONFIG lookup happens to be rendering right now:
The type-specific inputs render the same way the MongoDB sibling's own form does — one .map()
over FIELD_CONFIG[itemType], choosing a select for a field with options and a text
or number input otherwise. Nothing about that rendering logic is backend-specific, so it isn't repeated here.
The Leftover-Field Bug, Reproduced — and Not Auto-Corrected
typeFields is one shared object holding whichever fields the current type happens to use, and
nothing clears it when itemType itself changes. Select Book, type an author, then switch the
dropdown to Cd:
- Book is selected.
typeFieldsbecomes{ author: 'Ursula K. Le Guin' }. - The dropdown changes to Cd.
typeFieldsis untouched — still{ author: 'Ursula K. Le Guin' }, even though no author field is rendered anymore. - An artist value is typed.
typeFieldsis now{ author: 'Ursula K. Le Guin', artist: 'Kraftwerk' }. - The form submits with
itemType: 'Cd'and both fields in the outgoing document.
The MongoDB sibling's own version of this exact bug turned out to be harmless in practice — Mongoose's default
strict mode silently drops any field a document's own schema doesn't declare, so the leftover
author never actually reached storage. Checking the equivalent here, directly:
itemType, title, artist, and
a real, permanently stored author field that has no business being on a Cd at
all. Neither validateItem() nor the Chapter 3 Security Rules ever check for the absence
of unexpected fields — they only check that required fields are present. Firestore has no concept of a
document's own schema rejecting a field it doesn't recognize, because it has no concept of a document schema
at all. The stale field isn't a cosmetic display glitch here — it's a real, saved data-quality bug.
The Fix — Now a Correctness Fix, Not Just a UX One
The fix itself is identical to the MongoDB sibling's own one-liner:
validateItem(), not the Security Rules — was ever built to
reject fields it doesn't recognize. This one line is doing real correctness work, not just tidying up a
confusing input.
Closing this specific gap more thoroughly — rejecting a write outright if it carries a field no valid item
type actually uses — is a real option worth naming rather than building here: Firestore Security Rules support
exactly that check, via request.resource.data.keys().hasOnly([...]). Chapter 3's own rules don't
do this today; the one-line client-side reset below is what actually keeps this app's real data clean until
that stronger rule, if it's ever added, exists as a second line of defense.
Arrays From Text, and Why Numbers Need Real Conversion Here
Comma-separated text becomes a real array immediately before the write, exactly as the MongoDB sibling's own form does it — split, trimmed, and filtered, never stored as an array in state while it's still being typed:
releaseYear and runtimeMinutes need the same real-vs-string treatment the MongoDB
sibling's own form gives them — but the reason to bother is genuinely stronger here. Every DOM input produces
a string regardless of its declared type, and Mongoose's own Number SchemaType casts a valid
numeric string to a real number automatically on write, whether the client remembers to convert it or not.
Firestore does no such thing. Whatever JavaScript type a value has when it's handed to addDoc()
is exactly the type Firestore stores — a string stays a string, permanently:
where("runtimeMinutes", ">", 120), the kind a future "movies
over two hours" filter would need) compare values of matching type — a document whose
runtimeMinutes is stored as the string "125" is simply invisible to a query
filtering on the number 120, with no error raised anywhere to explain why it never shows up. The
conversion below isn't optional tidiness here; skipping it produces a real document that quietly breaks a
real future query.
Submitting the Form
Two separate catch blocks are deliberate. The first catches validateItem()'s own
synchronous throw — a specific, immediate message, no network request ever sent. The second catches whatever
addDoc() itself might reject, which in practice should only ever be a real Security Rules denial,
since anything validateItem() would have caught already stopped the function one line earlier.
Trying It in the Browser
With the Firestore project's rules deployed from Chapter 3, mounting
<AddItemForm onCreated={(item) => console.log('Created:', item)} /> is enough to test all
four types end to end: select each type, fill in its own fields, and confirm the logged item — then cross-check
directly in the Firebase console's own Firestore data viewer that all four documents exist in the
items collection, each with genuinely different fields, and none of them carrying a stray field
left over from whichever type was selected before it. Chapter 7 replaces this console log with the real
catalogue list this form is actually populating.
Hands-On Exercises
Build the full AddItemForm component with FIELD_CONFIG covering all four types, wire it into the app, and create one real item of each type through the actual UI, confirming each one via the Firebase console's Firestore data viewer.
📄 View solutionTemporarily remove the setTypeFields({}) reset from handleTypeChange, reproduce the leftover-field bug (fill a Book's author, switch to Cd, fill artist, submit), and confirm directly that the saved Cd document really does carry a stale author field this time — unlike the MongoDB sibling's own version of the same bug. Explain why the outcome differs.
📄 View solutionExplain why omitting the explicit Number() conversion on runtimeMinutes would be a genuinely more serious mistake in this course than in the MongoDB sibling, and describe a concrete, later scenario where the resulting bug would surface.
📄 View solutionChapter 4 Quick Reference
- FIELD_CONFIG — the identical declarative object the MongoDB sibling introduced, reused verbatim against this course's own Chapter 2 fields
- Shared vs. type-specific state — title/releaseYear/notes live in one piece of state; type-specific fields live in another, reset whenever itemType changes
- The leftover-field bug, this time uncaught — Firestore has no schema to silently drop an unexpected field the way Mongoose's strict mode does; the one-line reset is a real correctness fix here, not just a UX one
- Arrays from text — comma-separated input split/trimmed/filtered into a real array immediately before the write
- Numbers need real conversion — Firestore stores exactly the JS type it's handed, with no automatic casting; skipping Number() leaves a string field a future numeric query would silently never match
- Two error paths — validateItem()'s synchronous throw (fast, specific) vs. a Security Rules rejection surfaced through addDoc() (a real network round trip)
- Next chapter: Tags — storing, attaching, and filtering by tag on books specifically