I’ve been thinking about state management a lot lately. Every React project reaches a point where passing props down through component after component becomes a tangled mess. You start with useState, then graduate to useReducer or Context, but for truly complex applications, something more is needed. That’s where my mind has been—on finding a solution that feels less like a chore and more like a natural extension of how we think about data. This led me back to MobX, a library that promises to make state reactive and simple. Let’s talk about how it works with React.
The core idea is straightforward. You have some state—a user object, a list of items, application settings. In MobX, you make this state observable. This means MobX can watch it for changes. Your React components then become observers. They watch these observable pieces of state. When the state changes, only the components watching that specific piece of data update. It happens automatically. You don’t have to write complex logic to figure out what needs to re-render.
How do you set this up? First, you define your state. MobX works well with plain objects, but I often use classes for more structure. You use decorators or functions to mark properties as observable.
import { makeObservable, observable, action } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
addTodo: action,
completeTodo: action,
});
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
completeTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
export const todoStore = new TodoStore();
See how todos and filter are marked as observable? And the methods that change them are actions. This tells MobX, “Track changes here, and do it in a controlled way.” It’s clean and declarative.
Now, how does React know about this? You connect them using the observer function from mobx-react-lite. Wrap your component with it. The component will automatically subscribe to any observable it uses during its render.
import { observer } from 'mobx-react-lite';
import { todoStore } from './todoStore';
const TodoList = observer(() => {
const filteredTodos = todoStore.todos.filter(todo => {
if (todoStore.filter === 'completed') return todo.completed;
if (todoStore.filter === 'active') return !todo.completed;
return true;
});
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => todoStore.completeTodo(todo.id)}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
</li>
))}
</ul>
);
});
Notice something important? The filteredTodos logic is inside the component. But what if we computed it from the state itself? This is where MobX gets powerful. You can define computed values. These are values derived from your observable state. MobX caches them and only recalculates when the underlying observables change. It’s like a supercharged memoization.
Let’s improve our store.
import { makeObservable, observable, action, computed } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
addTodo: action,
completeTodo: action,
filteredTodos: computed, // This is a derived value
});
}
get filteredTodos() {
if (this.filter === 'completed') return this.todos.filter(t => t.completed);
if (this.filter === 'active') return this.todos.filter(t => !t.completed);
return this.todos;
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
completeTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
Now, the component becomes simpler and more efficient. It just reads todoStore.filteredTodos. MobX ensures the component re-renders only when the todos array or the filter changes, and the filteredTodos computation runs only when necessary. Can you see how this removes a layer of complexity from your components?
One common question is about async operations. How do you handle data fetching? MobX handles this gracefully with actions. You just mark your async function as an action, or use runInAction to update observables after the promise resolves.
import { runInAction } from 'mobx';
class UserStore {
userData = null;
loading = false;
error = null;
// ... observable declarations ...
async fetchUser(userId) {
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Use runInAction to batch state updates
runInAction(() => {
this.userData = data;
this.loading = false;
});
} catch (err) {
runInAction(() => {
this.error = err.message;
this.loading = false;
});
}
}
}
runInAction is crucial. It ensures all the observable updates (userData, loading, error) happen together in one transaction. This prevents your components from rendering with inconsistent, half-updated state.
So, when should you consider this integration? I find it shines in applications with dense, interconnected data. Think of a dashboard with multiple charts, filters, and data tables that all need to reflect the same underlying dataset. Or a complex form with many fields where changes in one section affect validation in another. MobX manages these relationships for you.
The mental model is different from Redux. You’re not dispatching actions to a reducer that returns new state. You’re modifying state directly, but in a controlled way via MobX’s system. For developers coming from an object-oriented background, or those who find functional immutability patterns cumbersome, this can feel much more intuitive. You write code that looks like plain JavaScript, and MobX provides the reactivity.
But is it all automatic magic? You still need discipline. Keep your actions focused and your state well-organized. The ease of mutating state can lead to spaghetti code if you’re not careful. Structure your stores logically, perhaps one per major domain of your app.
The result is an application that feels responsive. Components update precisely when they need to, without you manually orchestrating it. You spend less time optimizing re-renders and more time building features. The code is often shorter and easier to follow because you’re not writing as much boilerplate for connecting state to the UI.
I encourage you to try it on a smaller feature in your next project. Start with a complex form or a data-heavy page. Feel the difference when you don’t have to wire up a dozen useState or useReducer calls. See how computed values clean up your component logic.
What has your experience been with state management complexity? Have you found certain patterns that work better for your team? I’d love to hear your thoughts in the comments below. If this approach to simplifying React state resonates with you, please share this article with other developers who might be wrestling with the same challenges.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva