Marking Items Used

Food Tracker (React + Express)

Chapter 8 · Marking Items Used

Every earlier chapter built toward this exact moment: an item is finally used up, and the row created back in Chapter 5 needs to reflect that — without ever disappearing from the history Chapter 7 searches.

The Route

// routes/items.js (appended) router.patch("/:id/use", (req, res) => { const { id } = req.params; const result = db.prepare(` UPDATE items SET status = 'used', used_at = datetime('now'), expiry_date = NULL WHERE id = ? AND status = 'active' `).run(id); if (result.changes === 0) { return res.status(404).json({ error: "Item not found, or already used" }); } res.json({ id, status: "used" }); });

Two things worth noticing in that single query. First, expiry_date = NULL — the same nullable-not-deleted design Chapter 2 committed to from the start, now paying off: the row stays in the table forever, but its expiry date genuinely goes away, exactly the way Chapter 6's own alerts query (expiry_date IS NOT NULL) already expects. Second, AND status = 'active' in the WHERE clause — the update only actually touches a row that's currently active, which means marking an already-used item "used" again is a no-op rather than silently re-stamping used_at with a new, incorrect timestamp.

This must be a PATCH, never a plain link
A state-changing action like this must never be reachable via a plain GET — a browser's own link-prefetching, a crawler following every link on a page, or simply a user middle-clicking to open in a new tab could all trigger a GET request without the user ever intending to mark anything used. PATCH (or POST) requires the request to come from an explicit action — a button's onClick firing a real fetch call — never something a browser might do on its own while merely loading or navigating a page.

The React Action

function ExpiryDashboard() { const { alerts, loading, refresh } = useExpiryAlerts(); const markUsed = async (id) => { await fetch(`/api/items/${id}/use`, { method: "PATCH" }); refresh(); }; if (loading) return <p>Loading...</p>; if (alerts.length === 0) return <p>Nothing expiring soon.</p>; return ( <ul> {alerts.map((item) => ( <li key={item.id}> {item.name} — expires {item.expiry_date} <button onClick={() => markUsed(item.id)}>Mark Used</button> </li> ))} </ul> ); }
Chapter 6's refresh() finally earns its keep
Chapter 6 exposed refresh from useExpiryAlerts specifically for this moment, rather than only fetching once internally. Calling refresh() right after the PATCH succeeds re-runs the alerts query, and since the item's status is now 'used', it no longer matches that query's own WHERE status = 'active' condition — it disappears from the dashboard immediately, with no full page reload and no manual list-filtering logic on the frontend at all. The backend query is the single source of truth for "what's currently expiring soon"; the frontend just asks it again.
Wait-then-refresh, not optimistic updates
A more polished app might remove the item from the UI immediately, before the server even responds ("optimistic" updating), then roll back if the request fails. This course deliberately keeps the simpler approach — wait for the PATCH to actually succeed, then re-fetch — accepting a small, honest delay in exchange for never showing the user a state the server hasn't actually confirmed.

Where This Course Is Headed

Recipe lookup with TheMealDB next — an Express route and a React results component, matching items nearing expiry against real recipes.

Hands-On Exercises

Exercise 1

Explain what the AND status = 'active' clause in the UPDATE statement actually prevents, and what would go wrong without it if a user managed to click "Mark Used" twice on the same item.

📄 View solution
Exercise 2

Explain why marking an item used must never be implemented as a plain GET request, with a concrete example of how a GET-based version could be triggered unintentionally.

📄 View solution
Exercise 3

Explain how calling refresh() after a successful markUsed request causes the item to disappear from the dashboard, tracing exactly which earlier chapter's own query condition makes that work.

📄 View solution

Chapter 8 Quick Reference

  • Route: PATCH /api/items/:id/use — sets status='used', used_at, and clears expiry_date to NULL
  • Guard: AND status = 'active' in the WHERE clause makes re-marking an already-used item a safe no-op
  • Never a GET: a state-changing action must require an explicit request, not something a browser could trigger while merely loading a page
  • The payoff: refresh() from Chapter 6 re-runs the alerts query, which naturally excludes the now-used item — no manual frontend filtering needed
  • Deliberate simplicity: wait-then-refresh, not optimistic UI updates
  • Next chapter: Recipe Lookup with TheMealDB