MobX Computed Values: Derived State Without Manual Memoization
Use MobX computed values to derive state lazily. Learn how to define a computed cart total, verify lazy recomputation, and avoid common pitfalls.
16 Jul 2025, 20:13 UTC

Problem: Derived State Recalculates on Every Render
If you display a shopping-cart total that depends on an observable line-item list, recalculating that total inside every component render wastes CPU. MobX computed values solve this by tracking dependencies and recomputing lazily only when an observed value changes.
Worked Example: Cart Total
This example uses MobX 6 with makeAutoObservable, which works without decorator plugins.
import { makeAutoObservable, computed } from "mobx";
class CartStore {
items = [];
constructor() {
makeAutoObservable(this, {
total: computed,
});
}
addItem(name, price) {
this.items.push({ name, price });
}
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
export const cart = new CartStore();
Here items is an observable array. total is a computed getter: MobX records every observable read while total runs, so changing items marks total stale. The next read recomputes it once.
Using the Computed in React
import { observer } from "mobx-react-lite";
import { cart } from "./cartStore";
const CartTotal = observer(() => (
Cart total: {cart.total}
));
The observer wrapper subscribes to cart.total. The component re-renders only when the computed value changes, not when an unrelated observable updates.
How Dependency Tracking Works
When total executes, MobX notes that it read this.items. If you call cart.addItem, MobX marks total as stale. The function body does not run again until something reads cart.total. That lazy behavior avoids unnecessary work in large stores.
Verification: Confirm Lazy Recomputation
Run this in a Node script or browser console after installing mobx:
import { autorun } from "mobx";
import { cart } from "./cartStore";
let runs = 0;
autorun(() => {
runs += 1;
console.log("total:", cart.total);
});
cart.addItem("book", 12);
// Expected: autorun logs once for the initial read, then once after addItem.
// cart.total is not recomputed between those events unless it is read.
Check that runs increments only when cart.total is accessed after a dependency change, not on every addItem call when the value is never read.
Read-Only Enforcement
Computed values cannot be assigned. Attempting to set them throws:
try {
cart.total = 100;
} catch (error) {
console.log(error.message);
// Expected: a read-only assignment error.
}
Common Mistakes and Limits
- Reading non-observable values. If
totalreads a plain variable outside MobX, changes to that variable are invisible. Keep dependencies inside observable state. - Side effects inside a computed. Do not call actions or mutate observables inside
total. Computed values must be pure; mutations break tracking and can cause loops. - Circular computed dependencies. If computed A reads computed B and B reads A, MobX throws a dependency-cycle error at runtime. Flatten the graph.
- Heavy calculations. Computed is lazy, not free. If the derivation is expensive, consider whether an action-cached observable is more appropriate.
Computed vs Action-Cached Observable
| Approach | Best for | Trade-off |
|---|---|---|
@computed getter |
Pure derivation from observables | Read-only; recomputes lazily on dependency change |
| Action that writes to an observable | Expensive derivation used many times | Manual cache invalidation; mutable state |
Use a computed when the derived value is a function of current state. Use an action-cached observable only when recomputation cost is high and you can control invalidation explicitly.
Practical Check
After integrating, open your browser dev tools and count component renders while triggering unrelated store changes. If the component reading the computed does not re-render, the dependency tracking is working. If it re-renders constantly, verify that every value read inside the computed is observable and that the component is wrapped with observer.
Keep computed values small, pure, and read-only. Use actions for mutations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.