State Management Across the App
Food Tracker (React + Express)
Chapter 10 · State Management Across the App
Every feature so far has managed its own state: useExpiryAlerts, useDebouncedSearch, RecipeSuggestions' own useState. That worked fine when each feature only needed to know about itself. It stops being enough the moment one action needs to update several of them at once.
The Problem, Concretely
Chapter 8's markUsed called refresh() directly on the one hook it happened to have a reference to — useExpiryAlerts. But marking an item used should really affect three separate features at once:
- Alerts (Chapter 6) — the item should disappear, since it's no longer active.
- Search results (Chapter 7) — if the user's current search happens to include this item, its status should update to reflect "used."
- Recipe suggestions (Chapter 9) — an ingredient that's no longer expiring shouldn't keep influencing recipe matches.
Wiring markUsed to call three separate refresh functions directly works today, but it doesn't scale: every new feature that cares about item changes means going back and adding one more manual call at every single place items get mutated (add, mark used, and anything added later). That's a real, growing coordination cost, not a hypothetical one.
A Shared Change-Notification Context
Rather than wiring components directly to each other, one shared signal any component can both trigger and listen to:
version is deliberately just a number, not the actual item data — this Context coordinates when to refetch, it doesn't try to own or duplicate every feature's own data-fetching logic.
Wiring Every Consumer Through the Same Signal
Chapter 6's hook, updated to refetch whenever version changes, instead of only on mount:
And Chapter 8's markUsed, now calling the shared signal instead of one specific hook's own refresh function:
RecipeSuggestions and the search hook get the same one-line change: depend on version in their own useEffect, instead of being individually wired to whatever action happened to trigger the update.
O(features × mutations) wiring problem that only grows as the app grows. After this chapter, a mutation only needs to know about one thing: call notifyChange(). Every feature that cares about item changes subscribes to version on its own, entirely independently of whatever action happened to trigger the change. Adding a tenth feature later means that feature subscribes to version itself — it does not mean going back to add one more call inside markUsed, addItem, and everywhere else a mutation happens.
version causes every component consuming ItemsContext to re-render, even ones whose own displayed data didn't actually change as a result. At this app's own scale — a handful of features, infrequent mutations (a user adding or using an item, not hundreds of updates per second) — that's genuinely invisible. It would become a real performance concern in an app with many more Context consumers and much more frequent updates, where a more granular state library (Zustand, Jotai, or splitting into several smaller contexts) would be the more honest choice instead of one shared version counter.
Where This Course Is Headed
Deployment next — serving the built React app from the same Express process, and the environment configuration that goes with a real deployment.
Hands-On Exercises
Explain the concrete coordination problem with Chapter 8's original approach (markUsed calling refresh() directly) once a third feature, recipe suggestions, also needs to react to the same mutation.
📄 View solutionExplain why version is stored as a plain number rather than the actual items array, and what this Context is and isn't responsible for as a result.
📄 View solutionExplain the honest limit named in this chapter's own warn-box, and describe a scenario (in terms of app size or update frequency) where that limit would actually start to matter in practice.
📄 View solutionChapter 10 Quick Reference
- The problem: Chapter 8's direct refresh() call only reaches one hook — doesn't scale as more features need to react to the same mutation
- ItemsContext: holds one plain number, version, plus notifyChange() to bump it
- Every relevant hook: depends on version in its own useEffect, refetching its own data independently
- The real change: a mutation only calls notifyChange() once — it no longer needs to know which features are listening
- Honest limit: every Context consumer re-renders on every version bump — fine at this scale, a real cost at a much larger one
- Not a caching layer: each hook still fetches its own data from its own route; a library like React Query would add caching on top of this same idea
- Next chapter: Deployment