MobX Computed Values: Memoization and Lazy Evaluation in Practice
Learn how MobX computed values cache derived state, evaluate lazily, and prevent unnecessary React re-renders through structural equality checks.
13 Aug 2026, 18:12 UTC

The Problem: Repeated Derivation in Render
In React applications using MobX, a frequent performance bottleneck occurs when components derive data during render. Filtering a list of 10,000 items, sorting a collection, or computing aggregates on every render wastes CPU cycles when the source observables haven't changed. MobX computed values solve this by caching the result and only recomputing when the specific observables they depend on actually change.
How Computed Values Work
A computed value is a function wrapped in a getter (or created via toComputedValue) that MobX tracks for dependency access. Three mechanisms make it efficient:
- Lazy evaluation: The getter does not run until a reaction (like an
observercomponent orautorun) reads its value. - Caching: After the first run, the result is stored. Subsequent reads return the cached value without re-executing the getter.
- Structural equality notification: When dependencies change, the getter re-runs. If the new result is structurally equal to the previous one (by reference for objects/arrays, or value for primitives), MobX suppresses notifications to observers, preventing downstream re-renders.
Worked Example: Filtering and Sorting a Task List
The following store defines an observable array of tasks and two computed getters: one for completed tasks, another for tasks sorted by priority. The React component only re-renders when the filtered or sorted result actually changes.
import { makeAutoObservable, observer } from "mobx";
import React from "react";
class TaskStore {
tasks = [
{ id: 1, text: "Fix bug", completed: false, priority: 2 },
{ id: 2, text: "Write docs", completed: true, priority: 1 },
{ id: 3, text: "Deploy app", completed: false, priority: 3 },
];
constructor() {
makeAutoObservable(this);
}
get completedTasks() {
console.log("Computing completedTasks...");
return this.tasks.filter(t => t.completed);
}
get tasksByPriority() {
console.log("Computing tasksByPriority...");
return [...this.tasks].sort((a, b) => a.priority - b.priority);
}
toggleComplete(id) {
const task = this.tasks.find(t => t.id === id);
if (task) task.completed = !task.completed;
}
setPriority(id, priority) {
const task = this.tasks.find(t => t.id === id);
if (task) task.priority = priority;
}
}
const store = new TaskStore();
const TaskDashboard = observer(() => {
return (
Completed ({store.completedTasks.length})
{store.completedTasks.map(task => (
- store.toggleComplete(task.id)}>
{task.text}
))}
By Priority
{store.tasksByPriority.map(task => (
- store.setPriority(task.id, task.priority === 3 ? 1 : task.priority + 1)}>
{task.text} (priority {task.priority})
))}
);
});
export default TaskDashboard;
In this example, clicking a completed task toggles its completed flag. The completedTasks getter re-runs, but if the filtered array contents are identical (same objects in same order), MobX detects structural equality and the TaskDashboard component does not re-render. The console.log statements confirm when computation actually occurs.
Limits and Common Mistakes
1. Side Effects Break Caching
Computed getters must be pure. Adding side effects—API calls, state mutations, logging that mutates external state—causes unpredictable re-evaluation and can trigger infinite loops. If you need side effects when a computed value changes, use autorun or reaction instead.
2. Returning New Objects Defeats Structural Equality
If a computed getter returns a new array or object literal on every call (e.g., return { count: this.items.length }), structural equality fails because the reference changes. Downstream observers will re-render even when the logical content is identical. Return the same reference when possible, or use a custom equality comparer via computed(() => ..., { equals: comparer.structural }) (MobX 6+).
3. Memory Overhead from Unobserved Computeds
A computed value that is never read by any reaction still retains its dependency subscriptions if it was once observed. In long-lived stores, this can prevent garbage collection of large closure scopes. Use toComputedValue with explicit disposal or ensure computeds are only created when needed.
Verification Steps
- Open the browser console and interact with the component. Verify that
Computing completedTasks...logs only whentasksarray changes or a task'scompletedproperty toggles, not on unrelated state changes. - Open MobX DevTools (or
mobx-react-devtools). Inspect the dependency graph: the computed nodes should show as "stale" when no component observes them, and "active" when the dashboard is mounted. - Toggle a task's completion twice (on then off). The second toggle should not log a recomputation if the filtered result is unchanged, confirming structural equality suppression.
These steps confirm that computed values behave as lazy, cached, equality-aware derivations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.