MobX is one of the popular state management libraries. One of the great things about MobX is that we can store state in a simple data structure and allow the library to take care of keeping everything up to date. The MobX API is pretty simple; in fact, it has these four simple building blocks at its core:
- Observable
- Actions
- Computed
- Reactions
Observable
The idea is that when the data changes, the observable object notifies the observers. To define a property as observable, all we need to do is to use @observable
decorator:
class TodoStore {
@observable todos: Todo[]
}
Now When a new value is assigned to todos
array, the notifications will fire, and all the associated observers will be notified.
Actions
Action is a way to change an observable (update the state). To define an action, we decorate methods inside the store with @action
:
@action toggleTodo = (id: string) => {
this.todos = this.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
};
}
return todo;
});
};
Computed
Computed can be used to derive values from the existing state or other computed values:
@computed get info() {
return {
total: this.todos.length,
completed: this.todos.filter(todo => todo.completed).length,
notCompleted: this.todos.filter(todo => !todo.completed).length
};
}
Reactions
Reactions track observables from inside the store itself. In the example below if the action to set todos
is called, then it runs the second argument.
class TodoStore {
constructor() {
reaction(
() => this.todos,
_ => console.log(this.todos.length)
);
}
Creating a Simple Todo App with MobX and React
Now that we have talked about the main concepts, let's create a simple todo app using React, MobX and TypeScript:
So go to the terminal, create a directory then CD into it then type in this command to scaffold a new React project:
npx create-react-app . --typescript
For this project, I am using Bootstrap so let's add it as a dependency to the newly created project:
yarn add bootstrap --save
Now go to index.tsx
and import bootstrap.css
:
import "bootstrap/dist/css/bootstrap.css"
Now we'll install the needed dependencies:
yarn add mobx mobx-react-lite uuid @types/uuid --save
The next thing we have to do is to set experimentalDecorators
flag to true
in tsconfig.json
in order for the MobX decorators to compile properly:
{
"compilerOptions": {
// other stuff...
"experimentalDecorators": true
}
}
After all the above stuff is done, we have MobX ready to go. Now let's create a store inside the project src/stores/TodoStore.ts
. Add the following code to it:
import { observable, action, computed, reaction } from "mobx"
import { createContext } from "react"
import uuidv4 from "uuid/v4"
export interface Todo {
id?: string;
title: string;
completed: boolean;
}
class TodoStore {
constructor() {
reaction(() => this.todos, _ => console.log(this.todos.length))
}
@observable todos: Todo[] = [
{ id: uuidv4(), title: "Item #1", completed: false },
{ id: uuidv4(), title: "Item #2", completed: false },
{ id: uuidv4(), title: "Item #3", completed: false },
{ id: uuidv4(), title: "Item #4", completed: false },
{ id: uuidv4(), title: "Item #5", completed: true },
{ id: uuidv4(), title: "Item #6", completed: false },
]
@action addTodo = (todo: Todo) => {
this.todos.push({ ...todo, id: uuidv4() })
}
@action toggleTodo = (id: string) => {
this.todos = this.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed,
}
}
return todo
})
}
@action removeTodo = (id: string) => {
this.todos = this.todos.filter(todo => todo.id !== id)
}
@computed get info() {
return {
total: this.todos.length,
completed: this.todos.filter(todo => todo.completed).length,
notCompleted: this.todos.filter(todo => !todo.completed).length,
}
}
}
export default createContext(new TodoStore())
Now create a new folder called components in the src
directory and add TodoAdd.tsx
and TodoList.tsx
.
TodoAdd
import React, { useContext, useState } from "react"
import TodoStore from "../stores/TodoStore"
import { observer } from "mobx-react-lite"
const AddTodo = () => {
const [title, setTitle] = useState("")
const todoStore = useContext(TodoStore)
const { addTodo, info } = todoStore
return (
<>
<div className="alert alert-primary">
<div className="d-inline col-4">
Total items:
<span className="badge badge-info">{info.total}</span>
</div>
<div className="d-inline col-4">
Finished items:
<span className="badge badge-info">{info.completed}</span>
</div>
<div className="d-inline col-4">
Unfinished items:
<span className="badge badge-info">{info.notCompleted}</span>
</div>
</div>
<div className="form-group">
<input
className="form-control"
type="text"
value={title}
placeholder="Todo title..."
onChange={e => setTitle(e.target.value)}
/>
</div>
<div className="form-group">
<button
className="btn btn-primary"
onClick={_ => {
addTodo({
title: title,
completed: false,
})
setTitle("")
}}
>
Add Todo
</button>
</div>
</>
)
}
export default observer(AddTodo)
TodoList
import React, { useContext } from "react";
import TodoStore from "../stores/TodoStore";
import { observer } from "mobx-react-lite";
const TodoList = () => {
const todoStore = useContext(TodoStore);
const { todos, toggleTodo, removeTodo } = todoStore;
return (
<>
<div className="row">
<table className="table table-hover">
<thead className="thead-light">
<tr>
<th>Title</th>
<th>Completed?</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{todos.map(todo => (
<tr key={todo.id}>
<td>{todo.title}</td>
<td>{todo.completed ? "โ
" : ""}</td>
<td>
<button
className="btn btn-sm btn-info"
onClick={_ => toggleTodo(todo.id!)}
>
Toggle
</button>
<button
className="btn btn-sm btn-danger"
onClick={_ => removeTodo(todo.id!)}
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
};
export default observer(TodoList);
Both components use observer
which is an HOC to make the components observers of our store. So any changes to any of the observable will cause the React components to re-render.
Thatโs it ๐ Youโre now up and going with MobX in your React application.
Here's the source for the project.
Originally published at https://sirwan.info/blog
Top comments (2)
Cool!
To run on local i needed to change the constructor a little.
I used makeAutoObservable instead of the @observable.
Thanks your post, benefit a great deal.