MobX Computed Values: Derive State Instead of Duplicating It
MobX computed values replace manual state synchronization with cached, dependency-tracked derivations. Here's how they work, a concrete cart example, and where they go wrong.
07 Apr 2026, 09:39 UTC

Every MobX codebase eventually hits the same fork in the road: do you store a value in state, or do you derive it? Store too much and you end up writing synchronization code — updating a total field every time a price changes, and inevitably shipping a bug where one path forgets. Derive too little and your components re-implement the same calculation in three places. Computed values are MobX's answer, and used well they delete an entire category of consistency bugs.
The problem computed values solve
Say you have a shopping cart. The items are source data. The subtotal, item count, and free-shipping eligibility are all functions of that data. If you store the subtotal as its own observable, you now have two sources of truth and must keep them in agreement forever. If you compute it inline in each component, the logic drifts as features get added.
A computed value is a third option: a read-only, cached derivation that MobX keeps in sync automatically. You declare it once with computed (or the @computed decorator), and MobX re-evaluates it only when one of the observables it actually read has changed.
How the caching actually works
Two properties make computeds worth reaching for instead of plain getter functions:
- Dependency tracking. While a computed runs, MobX records every observable it touches. It re-runs only when one of those specific observables changes — an unrelated field on the same object won't trigger it.
- Memoization. The result is cached. If ten components read the same computed and nothing changed, the function body runs once, not ten times.
There's a caveat hiding in that first point: a suspended computed (one with no observers) stops being kept alive. MobX recomputes it lazily on next access. This is usually invisible, but it means computeds are not a place to put side effects — they may run zero times or many times, and they must stay pure. Anything that mutates state belongs in an action.
A worked example
Here's a minimal cart store using makeAutoObservable, which marks getters as computeds automatically (MobX 6+, which is the current major line):
import { makeAutoObservable } from "mobx";
class CartStore {
items = []; // { id, name, price, quantity }
constructor() {
makeAutoObservable(this);
}
get subtotal() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
get qualifiesForFreeShipping() {
return this.subtotal >= 50;
}
addItem(item) {
this.items.push(item); // auto-wrapped in an action
}
}Note that qualifiesForFreeShipping reads this.subtotal, another computed. Computeds can depend on other computeds, and MobX builds the dependency chain for you — changing an item's price re-evaluates subtotal, which in turn re-evaluates the shipping check, and only observers of the final value re-render.
To verify the caching behavior yourself, drop a console.log("recomputing subtotal") inside the getter, render a component that reads subtotal, then mutate an unrelated observable on the store. The log should not fire. Then change items and confirm it fires exactly once per change, no matter how many components read the value. This is a quick sanity check that your dependency graph is shaped the way you think it is.
Where computeds go wrong
The main failure mode is over-derivation through deep or wide dependency graphs. A computed that iterates a 10,000-item array re-runs in full when any single item changes, because MobX tracks at the property level, not the diff level. If that computed feeds a frequently updated UI, you've moved your performance problem rather than solved it. Practical mitigations:
- Keep observable structures flat. Deep nesting increases tracking overhead and makes dependencies hard to reason about.
- Split large derivations into smaller computeds so unrelated changes invalidate less work.
- For genuinely expensive aggregations over big collections, consider deriving incrementally in an action instead, accepting the manual-sync trade-off deliberately.
Also remember the purity rule: never mutate observable state inside a computed. MobX will warn in development (with enforceActions enabled), and in the worst case a mutation that feeds back into the computed's own dependencies produces an infinite loop. Side effects belong in actions or reaction/autorun.
The decision rule
The heuristic that holds up in practice: if a value can be expressed as a pure function of existing state, make it a computed. If it needs to trigger something (network calls, logging, navigation), that's a reaction. If it's genuinely new information from the outside world, that's observable state updated in an action. Keeping those three lanes separate is most of what makes a MobX app stay predictable as it grows.
Start by auditing one store in your current app: find any field that's updated in more than one action just to "keep it in sync," and replace it with a getter. That's usually the moment computed values click.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.