Implementing and Maintaining MobX Computed Values: A Practical Guide
Learn how to define, test, and troubleshoot MobX computed values in JavaScript or TypeScript projects. Follow a step‑by‑step guide that covers purity rules, caching behavior, and recovery strategies.
29 Dec 2025, 10:49 UTC

Desired Outcome
When you finish this guide you will be able to define computed values in a MobX store, guarantee they stay pure, understand how caching works, and know how to verify that they react correctly to observable changes. You’ll also have a checklist for common pitfalls and a recovery plan when a computed breaks.
Prerequisites
- Node.js 18+ and npm or yarn installed.
- A JavaScript or TypeScript project that already uses MobX (v6 or later).
- Basic familiarity with observables, actions, and reactions in MobX.
- Optional: a testing framework like Jest or Vitest.
Procedure
- Enable strict mode to enforce action boundaries and catch accidental mutations inside computed functions.
import { configure } from "mobx"; configure({ enforceActions: "always" }); - Create an observable state. For example, a simple counter.
import { makeAutoObservable } from "mobx"; class CounterStore { count = 0; constructor() { makeAutoObservable(this); } } export const counterStore = new CounterStore(); - Define a computed value. Use the class decorator style or the functional helper. The computed must be pure: no state mutation, no I/O, no side‑effects.
class CounterStore { /* ... */ get double() { return this.count * 2; } }Or, with the functional API:
import { computed } from "mobx"; class CounterStore { count = 0; double = computed(() => this.count * 2); } - Expose the computed in the UI. In a React component you can simply read it; MobX will track the dependency automatically.
import { observer } from "mobx-react-lite"; import { counterStore } from "./CounterStore"; const CounterDisplay = observer(() => ( Count: {counterStore.count}
Double: {counterStore.double} )); - Verify purity. Wrap the computed body in a test that attempts a mutation.
import { runInAction } from "mobx"; import { counterStore } from "./CounterStore"; test("computed is pure", () => { expect(() => { runInAction(() => { counterStore.double = 10; // illegal mutation }); }).toThrow(); }); - Check caching behavior. Log inside the computed and mutate unrelated observables to confirm it doesn’t recompute.
class CounterStore { /* ... */ get double() { console.log("double recomputed"); return this.count * 2; } } // In a component or test runInAction(() => { counterStore.count += 1; }); runInAction(() => { counterStore.otherObservable = 42; // unrelated });Only the first mutation should trigger the console log.
Expected Checks
- When
countchanges,doubleupdates immediately in the same tick. - Mutating an unrelated observable does not trigger recomputation.
- Attempting to mutate state inside a computed throws an error when
enforceActionsis set to "always". - Computed values are cached per store instance; re‑initializing the store clears the cache.
Recovery Options
- Pure‑function violation. If a computed throws, review the function body for accidental assignments or side‑effects. Move any logic that writes state or performs I/O into an
actionor areaction. - Performance spike. If a computed is heavy, consider splitting it into smaller computed values or using
reactionwith a throttled callback. Alternatively, move the expensive calculation outside the store and cache the result manually. - Unexpected recomputation. Verify that the computed only reads the intended observables. Use
mobx.traceor a custom wrapper to log dependency tracking during development. - Circular dependency. MobX throws an error if a computed depends on itself indirectly. If you hit this, refactor the dependency chain to break the cycle.
Limitations and Practical Checks
- Computed values are recalculated synchronously when any tracked observable changes. In very large dependency graphs, this can cause UI lag. Monitor performance with browser dev tools or a profiler.
- Because caching is per instance, sharing a store across components can lead to stale values if the store isn’t reset. Prefer scoped stores or context providers in React.
- MobX does not automatically debounce computed updates. If you need debounced derived state, wrap the computed in a
reactionthat writes to an observable after a delay. - When using TypeScript, ensure that computed getters are typed correctly. For example,
get double(): number { ... }helps the compiler catch type errors early.
Practical Checklist
| Step | Check |
|---|---|
| Define computed | Pure, no side‑effects |
| Enable enforceActions | throws on mutation |
| Test recomputation | only on relevant observable |
| Measure performance | no UI freeze on heavy logic |
| Verify caching | console log only on change |
Conclusion
Computed values are the backbone of declarative state in MobX. By following this guide you’ll write clean, efficient, and maintainable derived state that stays in sync with your observables while keeping your components free of boilerplate. Remember to enforce purity, monitor caching, and be mindful of performance when scaling up.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.