Jan 2026 • 5 min read
MobX Architecture Patterns
Structuring long-lived frontend state for complex operational systems.
Context
This came from operational products where the same entities kept surfacing everywhere that mattered: map canvases, dense tables, filter bars, headline dashboards, nested detail drawers. Nothing exotic about reuse on paper—in practice it meant one operator session could strand several views against the same underlying records.
Frontend state quit feeling like disposable page scaffolding. Parts of it lived across navigation, depended on overlapping async flows, and had to reconcile edits with pushes or pulls from upstream while something else on screen still showed older summaries. MobX rarely registered as “the bug.” Once organising ideas went soft around the stores, implicit subscriptions merely spread the fallout wider.
What I started noticing
Large bags of shared observable state became taxing to navigate when selection on a map, table sort order, stale filter chips, and background refresh hooks all leaned on loosely related siblings. Convenience led some stores toward mirroring REST or GraphQL envelopes almost field-for-field—which glued the observable tree to backend shapes instead of what the screens needed day to day.
Teams lost fluent answers for three routine questions: what is authoritative versus convenience, what is computed versus stored, and which write path actually caused observers to stir. Symptoms looked like needless invalidations or, worse, silently stale summaries while the primitives underneath had already moved.
The approach that worked better
Treat the observable graph as something you design on purpose, not something that grows by default.
Prefer normalised slices keyed by stable IDs so shared entities exist once while each view composes lists, selections, or map overlays from lookups those screens actually need. Derived presentation—rollup counts, visible subsets, enrichment for one panel—went into computed getters instead of being snapshotted into fresh fields alongside the source arrays.
Separate domain payloads from knobs that exist only for layout—hover highlights, ephemeral panel toggles, keyboard focus quirks. Combined early for speed, those concerns later resist untangling—and then every bug fix guesses which half belongs where.
Hold network calls, branching side effects, and asynchronous sequencing inside actions named after intent. Components subscribed to narrow slices—the few observables each tree actually depended on—rather than rooting into “the store” broadly when stray reads would rerun large subtrees downstream.
Rough ownership notes in the repo—or a terse diagram—saved arguments later when two features needed the same noun with different freshness rules.
Why this mattered
- Behavioural changes tended to isolate to the store neighbourhood or derivation that actually owed the update—not a brittle sweep across nested props.
- New screens borrowed existing IDs and computations instead of cloning half a tree under a bespoke key.
- Tracing causality grew legible enough for code review: an action adjusts observables; computeds ripple; observers render. Surprise usually meant a leaky dependency, not a missing framework knob.
- New contributors could answer “where does X live?” before changing behaviour without guessing their way across the repo first.
Trade-offs
Implicit dependency tracking catches people migrating from stacks built around explicit dependency lists. Debugging often means spotting which observable a computed touches—you read getters partly as miniature dependency graphs, because stack traces will not volunteer the dependency list for you.
What I would keep doing
Maintain a firm wall between domain state and UI ornamentation even when collapsing them earns a speedy pull request. Preserve the frontend as its own behavioural system instead of projecting backend responses one-to-one into observables—you still adapt at boundaries, then own the mutations that matter on the client.
Write down store ownership early. Ambiguity compounds quietly; retrofitting certainty after dashboards, maps, and tables already disagree is disproportionately costly.
Related Notes
- Map Core Architecture
- Map Data Contracts