MobX Computed Values: Deriving UI State Without the Boilerplate
MobX computed values automatically derive UI state from observables, memoizing results and eliminating manual sync code. This blog shows a concrete todo‑list example, verification steps, and the trade‑offs to watch for.
30 Jan 2026, 19:12 UTC

The problem: manual sync code clutters components
When a component needs a filtered list, a total price, or any value that depends on observable data, the naïve approach is to recompute on every render or to subscribe to changes manually. Both paths add boilerplate, risk stale UI, and make the data flow harder to follow.
Thesis: computed gives you automatic, memoized derivation
MobX’s computed decorator (or function) creates a derived observable that recalculates only when its tracked dependencies change. The result is cached, so expensive work runs once per dependency update, and components that read the computed value react automatically.
Why computed values matter
- Automatic dependency tracking – MobX records every observable read inside the getter, building a precise dependency graph.
- Memoization – The cached value is reused until a dependency changes, preventing unnecessary re‑renders.
- Works with class stores and plain objects – You can migrate existing code without a full rewrite.
How they work under the hood
When a computed getter runs, MobX wraps it in a Reaction. Every observable accessed during that run is registered as a dependency. On subsequent reads, MobX returns the cached value. If any dependency fires a change notification, the reaction marks the computed as stale and re‑evaluates on the next read.
Worked example: filtering a todo list
Assume a simple store with an observable array of todos and a filter string. The filtered list is a computed value.
// store.js
import { makeAutoObservable, computed } from 'mobx';
export class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
add(text) {
this.todos.push({ id: Date.now(), text, done: false });
}
toggle(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
@computed get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(t => !t.done);
case 'completed':
return this.todos.filter(t => t.done);
default:
return this.todos.slice();
}
}
}
A React component can now read store.filteredTodos directly; it will re‑render only when todos or filter changes.
// TodoList.jsx
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import { StoreContext } from './StoreProvider';
export const TodoList = observer(() => {
const store = useContext(StoreContext);
return (
<ul>
{store.filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => store.toggle(todo.id)}
/
>
{todo.text}
</li>
))}
</ul>
);
});
Verification steps
- Run
npm install mobx mobx-react-lite(requires Node ≥ 14, write permission tonode_modules). - Start the dev server (
npm run devornpx vite). - Open the browser devtools console and add a todo; observe that the list updates instantly.
- Change the filter via a UI control; the list updates without a full re‑render of unrelated components.
- In the React DevTools Profiler, confirm that
TodoListrenders only whenfilteredTodoschanges.
Trade‑offs and debugging tips
- Hidden dependencies – If a computed reads an observable indirectly (e.g., via a helper that accesses another observable), the dependency graph may miss it, leading to stale values. Keep getters pure and explicit.
- Heavy logic – Expensive calculations inside a computed run on every dependency change. Move truly heavy work to an action that updates a separate observable, or debounce the source observable.
- Mixing plain data – Adding a plain JavaScript object to
todoswithoutmakeAutoObservablebreaks reactivity. Wrap new items withobservable(object)or usemakeAutoObservableon the store.
Actionable next step
Pick a component that currently derives state in useMemo or a useEffect and replace it with a @computed in the store. Run the verification steps above; you should see fewer renders and cleaner component code. If you hit a stale UI, add a console.log inside the computed getter to confirm which observables are being tracked.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.