Using MobX Reactions to Minimize Unnecessary UI Updates in React
Learn how MobX reactions (autorun, when, reaction) can synchronize side‑effects only when observed observables change, reducing renders in a todo list example.
15 Jul 2025, 12:38 UTC

Why frequent re‑render hurt performance
In a large React app, every state change can trigger a render pass through the component tree. When state is mutated directly in many places, components that only need a subset of data may still re‑render, wasting CPU cycles.
How MobX reactions synchronize side‑effects
MobX provides three reaction primitives:
autorun– runs a function automatically and re‑runs it whenever any observable it reads changes.when– waits for a predicate to become true, then runs an effect once.reaction– combines a data‑extraction function with an effect function, letting you control which observables are watched.
Because reactions track only the observables they actually read, they avoid the overhead of re‑running on every state mutation.
Worked example: a todo list that updates the document title
First, install the packages:
# Run in your project root
npm install mobx mobx-react-lite
Define a simple observable store:
import { makeAutoObservable } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggle(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
export const todoStore = new TodoStore();
Create a React component that uses observer from mobx-react-lite and an autorun reaction to keep the document title in sync:
import React, { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { autorun } from "mobx";
import { todoStore } from "./todoStore";
const TodoApp = observer(() => {
// Side‑effect: update document title when the todo list length changes
useEffect(() => {
const disposer = autorun(() => {
document.title = 'Todos (' + todoStore.todos.length + ')';
});
return disposer;
}, []); // empty deps – the reaction is managed by MobX
return (
Todo List
{
if (e.key === "Enter") {
todoStore.addTodo(e.target.value);
e.target.value = "";
}
}}
/>
{todoStore.todos.map(todo => (
-
todoStore.toggle(todo.id)}
/>
{todo.text}
))}
);
});
export default TodoApp;
In this example:
- The
autorunreaction reads onlytodoStore.todos.length. Adding a todo changes the length, so the title updates. - Typing in the input does not change any observable, so the reaction does not re‑run and the component does not re‑render beyond the local input state.
You can verify the behavior with React DevTools Profiler: components wrapped with observer will show a render spike only when the todo list length changes.
Trade‑off and limitation
Reactions introduce implicit dependencies. If a reaction accidentally mutates an observable it is watching, you can create an infinite loop:
// Dangerous pattern – avoid
autorun(() => {
if (todoStore.todos.length > 5) {
// Mutating the observed observable inside the reaction
todoStore.todos.push({ id: Date.now(), text: "auto", completed: false });
}
});
To keep reactions safe:
- Never modify an observable that is read inside the same reaction.
- Wrap asynchronous mutations in
runInActionto ensure MobX tracks them correctly. - Prefer
whenorreactionwhen you need more control over what triggers the effect.
Another practical limitation is debugging: because the dependency graph is built at runtime, tracing why a reaction fired can be harder than reading an explicit useEffect dependency array.
Actionable closing
- Identify side‑effects that should run only when specific data changes (e.g., updating the document title, sending analytics, syncing with localStorage).
- Replace ad‑hoc
setTimeoutoruseEffectwith a MobXautorunorreactionthat observes the minimal set of observables. - Run the app, open React DevTools Profiler, and confirm that components re‑render only when the tracked observables change.
- Review any reaction for accidental mutations; if you find one, move the mutation outside the reaction or guard it with a flag.
By scoping reactions to the exact observables they need, you keep UI updates predictable and avoid unnecessary renders in large React applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.