State Architecture in Practice
Progressive complexity, production patterns, and what senior engineers get wrong
Progressive Complexity
The same idea—put state as close as possible to where it's used—applies when a feature grows. Below we take one feature, a product filter, and build it five ways: from state that lives in a single component to state that crosses the whole app. Each step adds coordination needs and shows when to reach for the next pattern (lifted state, URL, server, global).
Example 1: Local State
Simple100 products, single page
The simplest case: a filter that only affects one component.
- Widget A
- Widget B
- Gadget X
- Gadget Y
- Tool Alpha
- Tool Beta
Why this works
When this breaks
Production Patterns
Here's how I actually use these patterns in production applications.
Scenario A: E-commerce Checkout
- PCI compliance requires no card data in client state
- Cart must persist across sessions and devices
- Checkout flow has 4 steps with form state
- Need optimistic updates for UX
Cart data: Server state with React Query
Form state: Local state per step
Current step: URL parameter
Payment token: Ephemeral, never stored
Accepted: Slightly more complex data flow. Gained: Clear separation of concerns, easier to audit for compliance, natural boundaries for testing.
Scenario B: Collaborative Dashboard
- Multiple users editing same data in real-time
- Heavy calculations for chart rendering
- Need to support offline mode
- Filter/sort state should be shareable
Data: WebSocket + Zustand with optimistic updates
Filters: URL state (shareable links)
Chart config: Local state per widget
UI state: Local state (sidebar open, etc.)
Initially used Context for all data. Performance tanked. Moved to Zustand with selectors. Should have started there.
Decision Signals
Each transition in the examples above is triggered by a specific kind of friction. Here are the concrete signals that tell you it's time to move to the next level — not earlier.
- →Two sibling components need to read the same value
- →A parent needs to react to something that happens in a child
- →You find yourself duplicating useState in two places and keeping them in sync
- →The user would be frustrated if a browser refresh cleared the state
- →You want to share a specific view with another person via a link
- →Analytics should capture the actual filter/search combination users are using
- →The back button should restore the previous filter, not go to the previous page
- →The dataset is too large to filter/sort client-side (>1k items, or growing)
- →Multiple URL params combine in ways that need the server to compute the result
- →The same data is fetched in multiple places and should be cached
- →A user action in one feature needs to immediately update what another unrelated feature displays
- →State is genuinely cross-cutting: the same value affects 5+ components across different subtrees
- →You need to write to state from outside the React tree (WebSocket handler, service worker)