Mutable State with Mobx
Mobx is a popular alternative to Redux, an immutable state management system usually used with React as the view layer. However Mobx is mutable, so how is React aware of when changes have been made?
Let’s assume a very simple store that stores an array of todos.
interface Todo {
task: string;
complete: boolean;
}
type State = Todo[]
Let’s create a store for such a state and allow the ability to append new todos, as well as complete them by giving an index. In Redux you would typically create a reducer like this:
const todoReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
task: action.task,
complete: false,
}
];
case 'COMPLETE_TODO':
return todos.map((todo, index) => {
if (index === action.index) {
return {
...todo,
complete: true,
};
}
return todo;
})
default:
return state;
}
};
You would create your store, merge reducers and then in your component you would then dispatch actions for the reducer to handle:
store.dispatch({ type: 'ADD_TODO', task: 'Feed the dog' });
store.dispatch({ type: 'COMPLETE_TODO', index: 3 });
With this immutable approach, it is easy for React to know about changes as we are returning a new array every time. Once the comparison is made, React can then update the virtual DOM accordingly. With primitive types this happens automatically but with objects and arrays we need to create them ourselves. We can do this in a variety of ways such as the spread operator (...
), Object.assign
or using array methods like map
.
In Mobx, we would create the store like this:
class TodoStore {
@observable todos = [];
@action addTodo = task => {
this.todos.push({ task, complete: false });
}
@action completeTodo = index => {
this.todos[index].complete = true;
}
};
You can see that we are operating on the same array. So how does React know when state changes? With reactive programming, observables notify observers of changes (see observer pattern). We can make components observables with ease using libraries like mobx-react
and mobx-vue
.
However how is it that we are able to achieve this without calling custom methods or explicitly calling something? How is it that we can write this.todos[0] = { task: 'Feed the cat', complete: false }
and it just work.
When the @observable
decorator is used, the array becomes an ObservableArray (you can also be explicit with observable.array()
). This works recursively so that all future values of an array will also be observable. The getters, setters and all other methods on a native array are then simulated which perform the same behavior on the containing array, plus report on changes. Other non-primitive types also exist like ObservableObjects and ObservableMaps.
With the additional code, Mobx is not as lightweight as Redux, but it does drastically increase simplicity with this abstraction, despite looking like magic to those that don’t dive into the source.