Optimizing Derived State with MobX Computed Values
Stop manually managing useMemo dependencies. Learn how MobX computed values provide automatic caching and lazy evaluation to keep your React UI performant and your state logic declarative.
18 Jan 2026, 01:59 UTC

The Problem: Manual Memoization and Render Bloat
In many React applications, developers calculate derived state—such as filtered lists, totals, or formatted strings—directly within the component body. This means the calculation runs on every single render, regardless of whether the underlying data has changed. To fix this, developers often reach for useMemo or external selector libraries.
While these tools work, they introduce manual dependency arrays. If you forget to add a dependency, your UI shows stale data; if you add too many, you lose the performance benefit. This creates a maintenance burden where the developer must manually sync the state logic with the memoization logic.
Computed: Declarative, Cache-Aware Derivation
MobX solves this via computed values. A computed value is a piece of state that can be derived from other observables. It is designed around four core principles:
- Pure: It only reads observables and returns a value without modifying state.
- Lazy: It does not execute unless it is actually being observed by a reaction (like a UI component).
- Cached: The result is stored. If the underlying observables haven't changed, MobX returns the cached value instantly.
- Fine-grained: MobX tracks exactly which observables are accessed during the first execution, ensuring it only re-evaluates when those specific values change.
How the Dependency Graph Works
When a computed value is accessed, MobX performs a "discovery" phase. If you have a computed property that calculates a total from an array of items, MobX notes that this computed value depends on that specific array.
If another observable in your store (e.g., a user's preferred theme color) changes, MobX knows that the total calculation is unaffected. The cached value remains valid, and the components relying on that total do not re-render. This eliminates the need for manual dependency arrays entirely.
Worked Example: Shopping Cart Totals
The following example demonstrates how to use makeAutoObservable to define a store where the total price is a computed value. This ensures the calculation only runs when the items array is modified.
import { makeAutoObservable, computed } from 'mobx';
import { observer } from 'mobx-react-lite';
class CartStore {
items = [
{ name: 'Keyboard', price: 100 },
{ name: 'Mouse', price: 50 },
];
constructor() {
// makeAutoObservable automatically infers 'computed' for getters
makeAutoObservable(this);
}
// This is a computed value because it is a getter
get total() {
console.log('Calculating total...'); // Used to verify caching
return this.items.reduce((sum, item) => sum + item.price, 0);
}
addItem(name, price) {
this.items.push({ name, price });
}
}
const cartStore = new CartStore();
const CartComponent = observer(() => (
Your Cart
{cartStore.items.map((item, i) => - {item.name} - ${item.price}
)}
Total: ${cartStore.total}
cartStore.addItem('Cable', 10)}>Add Cable
));
Verification Steps:
- Run the component; you will see "Calculating total..." in the console once.
- Trigger a re-render of the parent component (via a separate state change); the console will not log the calculation again because the value is cached.
- Click "Add Cable"; the
itemsobservable changes, invalidating the cache and triggering a single re-calculation.
Trade-offs and Limitations
- Pure Function Requirement: Computed values must be pure. Performing side-effects (like API calls or modifying other observables) inside a getter will break the reactivity system and can lead to infinite loops.
- Deep Dependency Chains: While powerful, nesting computed values (Computed A depends on Computed B, which depends on Observable C) can make debugging difficult. If a value changes unexpectedly, you may have to trace through several layers of derivation.
- Memory Overhead: Because MobX caches the results of computed values, very large numbers of computed properties in a massive store can increase memory usage.
Actionable Takeaway
To optimize your MobX stores, identify any logic currently living inside useEffect or component bodies that transforms state into a view-ready format. Move this logic into getters (computed values) within your store. This shifts the burden of dependency tracking from the developer to the MobX runtime, reducing boilerplate and preventing unnecessary UI churn.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.