Item History & Live Search-as-You-Type
Food Tracker (React + Express)
Chapter 7 · Item History & Live Search-as-You-Type
Every item ever added stays in the items table forever — Chapter 2's own design decision, active or used. This chapter surfaces that whole history, searchable in real time as the user types.
The Search Route
Deliberately no status filter — unlike Chapter 6's own alerts query, this route searches the entire history, active and used items both, since re-adding something bought before is exactly the case this search exists for. '%' || ? || '%' wraps the parameter in SQL wildcards while still keeping it fully parameterized — the same SQL-injection protection from Chapter 2 applies here without any extra effort.
nameLower, purely because Firestore has no native case-insensitive substring search at all — every item's name has to be duplicated into a second, search-friendly field just to make matching work. SQLite's own LIKE operator is case-insensitive for ASCII text by default, with no shadow field, no duplicated data, and no extra write-time bookkeeping required. This is the exact same honest advantage Food Tracker (Django) already claimed for its own icontains lookup — a genuine, real SQL win, not specific to any one framework built on top of SQL.
'%' || ? || '%') can never use a standard database index efficiently, even if one existed on name — the database has no choice but to check every row, since a match could start anywhere in the string. At this app's own realistic scale (Chapter 6's own point, repeated here), that cost is genuinely invisible. It would become a real, different problem at a scale large enough to need dedicated full-text search — a tool like SQLite's own FTS5 extension, or an external search service, neither of which this course builds.
A Reusable Debounce Hook
Firing a search request on every single keystroke would flood the server with requests for a query the user hasn't finished typing yet. Debouncing delays the actual fetch until typing pauses:
return () => clearTimeout(timeoutId), every keystroke would still schedule its own timer, and every one of those timers would eventually fire — the delay would only push the flood of requests later, not prevent it. Because useEffect re-runs this whole function on every query change, each run's cleanup cancels the previous run's still-pending timer before scheduling a new one — the same "always clean up what the last effect run started" discipline Chapter 4's camera-stream cleanup already taught, applied here to a timer instead of a media stream.
The Search Component
Where This Course Is Headed
Marking items used next — a React action and an Express PATCH endpoint, tying directly back into both this chapter's own search results and Chapter 6's own alerts dashboard.
Hands-On Exercises
Explain why this course's search route needs no shadow field the way Food Tracker (React + Firebase)'s nameLower field does, and name the SQLite feature responsible.
📄 View solutionExplain what would happen if useDebouncedSearch's useEffect omitted its cleanup function, and why the fix is described as "the same lesson" as Chapter 4's camera-stream cleanup.
📄 View solutionExplain why the search route deliberately has no status filter, unlike Chapter 6's own alerts route.
📄 View solutionChapter 7 Quick Reference
- Route: GET /api/items/search?q=... — SQL LIKE '%'||?||'%', no status filter, the full history
- Real SQL advantage: SQLite's LIKE is case-insensitive for ASCII by default — no nameLower shadow field needed, unlike the Firebase sibling
- Honest limit: a leading wildcard can't use an index — invisible at this app's scale, a real cost at a larger one
- useDebouncedSearch: a reusable hook, delay tunable, cleanup cancels the previous pending timer on every keystroke
- Same lesson as Chapter 4: always clean up what the previous effect run started
- Next chapter: Marking Items Used