Streamlining React Development with MobX for State Management
When it comes to React development, managing state can feel like juggling a dozen spinning plates—add a new feature, and suddenly your simple setup is feeling more like spaghetti code. If you’re looking for a state management solution that’s simpler to implement than Redux and brings powerful reactivity to your app, MobX might be exactly what you need.
Let’s explore why MobX can be a game-changer, especially for developers who want to keep their code straightforward and avoid the headaches of complex boilerplate. We’ll walk through the essentials, an example, and a few best practices to help you understand what makes MobX a smart choice for your React projects.
Why MobX?
React developers often start with small, manageable state handled by useState
or useReducer
. However, as your application grows, so does the complexity of sharing and syncing state across components. MobX stands out here for a few reasons:
Automatic Reactivity: MobX observes your data and triggers updates automatically, reducing the need for custom handlers.
Less Boilerplate: With MobX, you avoid the often complex setup required by Redux. There are no reducers or action types—just straightforward state management.
Scalability: MobX handles both small and large-scale apps well, making it a versatile solution.
In short, MobX focuses on reactivity and simplicity, aiming to let you focus more on your app logic and less on tedious setup.
Getting Started with MobX
First things first: to use MobX with React, you’ll need to install a couple of packages:
npm install mobx mobx-react-lite
The code snippet above works if you’re using only functional components, if your application is a bit old and you have some class components then you should consider installing an alternative version that supports class components.
npm install mobx mobx-react
The core concept in MobX is the “store.” A store is where we define the app’s state and the actions to update that state. Let’s go through the essentials: observables, actions, and computed values.
MobX Essentials
1. Observable State
Think of observables as reactive pieces of data. Whenever an observable changes, MobX automatically tracks this change and re-renders the components that use it.
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
}
Here, we created a basic TodoStore
with a list of todos. By wrapping it with makeAutoObservable
, MobX automatically tracks and reacts to any changes in the todos
array.
2. Actions
Actions are methods that modify state. They let MobX know which parts of your code are meant to make state updates. Let’s add an action to add a new todo to our list.
addTodo = (todo) => {
this.todos.push(todo);
};
Adding this inside TodoStore
allows us to handle state updates in an organized way. MobX will know that this is an intentional change and automatically manage any necessary re-renders.
3. Computed Values
Computed values derive information from the current state without directly modifying it. Think of them as live calculations. For example, if we want to track the number of unfinished tasks:
get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.completed).length;
}
Now, anytime we reference unfinishedTodoCount
, MobX will recalculate it based on the current todos
array.
Building a Simple Example App
Let’s pull these concepts together with a straightforward React app—a basic to-do list. This will show you how MobX automatically updates components when data changes, sparing you from manually managing these updates.
1. Set up the Store
import { makeAutoObservable } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
toggleTodo = (index) => {
this.todos[index].completed = !this.todos[index].completed;
};
get unfinishedTodoCount() {
return this.todos.filter((todo) => !todo.completed).length;
}
addTodo(text) {
const newTodo = { id: Date.now(), text, completed: false };
this.todos.push(newTodo);
}
deleteTodo(id) {
this.todos = this.todos.filter((todo) => todo.id !== id);
}
toggleComplete(id) {
const todo = this.todos.find((todo) => todo.id === id);
if (todo) todo.completed = !todo.completed;
}
get completedTodos() {
return this.todos.filter((todo) => todo.completed).length;
}
}
const todoStore = new TodoStore();
export default todoStore;
Here you would find that we did not only declare the the todoStore
class but we also instantiated it, before exporting the instance of the the object.
This is because we are using the singleton pattern for consistent state manage such that, we have only one instance of our store used across the application, that way updates can easily be tracked and synchronised smoothly.
Unlike exporting the class and creating multiple instances across the application, it would easily make the application state complex and hard to maintain.
2. Use the Store in a React Component
Now, let’s connect this store to a component.
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import todoStore from "../store/todo-store";
const TodoList = observer(() => {
const [newTodo, setNewTodo] = useState("");
const handleAddTodo = () => {
if (newTodo.trim()) {
todoStore.addTodo(newTodo);
setNewTodo("");
}
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">To-Do List</h1>
<div className="mb-4">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="New task..."
className="border p-2 mr-2"
/>
<button
onClick={handleAddTodo}
className="bg-blue-500 text-white px-4 py-2"
>
Add
</button>
</div>
<ul className="space-y-2">
{todoStore.todos.map((todo) => (
<li key={todo.id} className="flex items-center space-x-4">
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleComplete(todo.id)}
className="mr-2"
/>
<span
className={todo.completed ? "line-through text-gray-500" : ""}
>
{todo.text}
</span>
<button
onClick={() => todoStore.deleteTodo(todo.id)}
className="text-red-500 hover:underline"
>
Delete
</button>
</li>
))}
</ul>
<div className="mt-4 text-gray-600">
Completed tasks: {todoStore.completedTodos} / {todoStore.todos.length}
</div>
</div>
);
});
export default TodoList;
With MobX’s automatic reactivity, any change to todoStore.todos
(like adding a new item or toggling completion) immediately updates the component, making the UI responsive with minimal setup.
Scaling MobX in Larger Applications
As applications grow, you may want to organize state across multiple stores (e.g., TodoStore
, UserStore
, SettingsStore
). MobX is well-suited to handle this setup. Instead of a single, monolithic store, multiple stores can encapsulate different slices of your app’s logic, keeping it modular and easier to maintain.
Additionally, MobX integrates well with libraries like React Query for async data, or Axios for making API requests, allowing you to use MobX for local state and other libraries for server state as needed.
Best Practices for Working with MobX
Here are a few tips to keep your MobX code clean and efficient:
Use Observables Sparingly: Only make properties observable if they’ll change over time.
Leverage Computed Values: Calculated data, such as
unfinishedTodoCount
, should ideally be defined as computed properties for optimized performance.Avoid Overly Complex State: MobX is powerful but can become hard to debug if your stores contain too much logic. Keeping store logic focused on simple, direct state management is a good habit.
Wrapping Up
MobX can make state management in React much easier and more efficient by taking care of reactivity for you. With MobX, you avoid writing boilerplate code and can scale with ease as your application grows. Next time you’re deciding on a state management tool, consider giving MobX a shot—its simplicity and power might surprise you.
Give it a try in your next project, and enjoy a more streamlined React development experience!
Subscribe to my newsletter
Read articles from Adedeji Agunbiade directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by