We frequently ask ourselfs the following question when starting a project using React, React Native, or any other Js framework: How do we manage application's state? How can manage data flows in a better way?
Managing application's state is arguably the most challenging part. That's why there are so many state management libraries available and more coming around every day (Redux, Redux Saga, Redux Rematch … etc).
Redux is a pretty good solution and it's awesome in languages and environments which really support it, However, in JavaScript ecosystem, it has some downsides that led developers to seek an alternative to it :
- No Runtime Type-Checking: instead we have to write a bunch of interfaces to support type checking.
- It requires immutable data structures. JavaScript has gotten better at dealing with this kind of data, using redux is somewhat awkward and verbose (Taking the existing state, make 3 dots, then new value …etc)
- It’s more difficult to track everything you need to change throughout the chain of events.
- It has a big learning curve, you need to master concepts like : dispatcher, reducers, action, creators, thunks, sagas, epics, and middleware. Which means more setup.
💡 Immutable data structure are data structures, like lists, arrays, sets etc., which cannot be changed, meaning that the values inside them can't be added, removed, moved or swapped. Instead of changing the data structure you make a new version of the data structure which is a separate value.
In this step-by-step guide, we will explore MobX State Tree (MST) which is the most popular Redux alternative and powerful state management library which you can use from small to enterprise-grade applications and it’s very easy to plug and play.
Before we start, let's first understand what Mobx State Tree is and some reasons why you should use it.
Mobx State Tree
MobX State Tree (MST) is a library built on MobX that helps you organize your application and create components models for your data in a very structured manner. It offers multiples features and a modern way for state management, everything is packaged in a lightweight library, with no more extra dependency.
In short, MobX is a state management "engine", and MobX State Tree gives it structure and common tools you need for your app.
Why should we use Mobx State Tree ?
Mobx State Tree has many features compared to other state management, here are the most important :
- ✔️ Runtime type checking: Helps write clean code and prevents developers from incorrectly allocating data to a tree, TypeScript can then automatically infer static types from runtime types.
- 🔀 Tracking Data Changes : Every change made in the state is tracked and you can create a snapshot of your state at any time, which is helpful in debugging or logging state changes...etc.
- 🎯 Well Organised Code : Instead of messy code everywhere in the app, MST provides centralized stores to quickly access and exchange data.
- 🔒 Encapsulation: the data is stored in a mutable form, which can be mutated easily and only by calling “actions” inside the store.
- ⚒️ Fast Learning Curve : we can easily onboard new developers in existing project which was not the case in projects using Redux.
- 🔥 Less Boilerplate : MST offers far less boilerplate code and superior speed when compared to Redux!
In the next section, we will discuss and see how to install the library, then we will implement an example of a Todo mobile app using React Native and Mobx State Tree.
Setup
As we mentioned earlier, The Mobx State Tree is built on MobX, MobX is the state management and MST gives us a structure to store our data. For that, we need to install Mobx and Mobx State Tree with the following commands :
- NPM :
npm install mobx mobx-state-tree --save
- YARN :
yarn add mobx mobx-state-tree
Tutorial
In this tutorial, we are going to build this mobile application to showcase Mobx State Tree’s features.
⚠️ Note that the setup and the examples we will cover later are valid for any Js Framework.
Let's get started by creating our model.
How to create a Model ?
A Model in MobX State Tree is a way to define the structure of an object, it is similar to classes in object-oriented programming, but they are designed to work with the MobX library for state management.
We can create a model by using types.model , it takes a name and an object of props.
MST provides all the necessary types for the development: array, map, maybe, union and many more, you can access the exhaustive list using the link bellow : Read more
//Task.ts
import { types } from 'mobx-state-tree';
export const TaskModel = types.model('Task').props({
id: types.identifier,
task: types.string,
isCompleted: types.optional(types.boolean, false),
creationDate: types.Date,
});
- Types.identifier : Means that each id must be unique within the entire tree.
- Types.optional : If no value is provided , the default value false will be used instead.
- Types.Date : Creates a type that can only contain a javascript Date value.
After that , we need to create a Store to hold instances of our models.
How to create a Store ?
The Store is also a Model that represents a branch of the tree and holds instances of atomic Models.
For this tutorial , we will need 2 props :
- tasks : which is an array of TasksModel.
- filter : an optional of a filterType that we will create later to set filters of our dropdown, with a default value SHOW_ALL.
//TasksStore.ts
import { TaskModel } from './Task';
const TasksStore = types
.model('TasksStore')
.props({
tasks: types.array(TaskModel),
filter: types.optional(filterType, SHOW_ALL),
})
How to modify the data ?
We can modify the tree node in the MST only by actions and the data can’t be modified from outside. By doing so we are protecting our data.
const TasksStore = types
.model('TasksStore')
.props({
tasks: types.array(TaskModel),
filter: types.optional(filterType, SHOW_ALL),
})
.actions(self => ({
addTask(task: Task) {
self.tasks.unshift(task);
},
removeTask(task) {
destroy(task);
},
resetTasks() {
applySnapshot(self, {});
},
setFilter(filter) {
self.filter = filter;
},
}));
Let’s cover each line of the code above🔝
Did you notice self ? It’s an object constructed when an instance of our Store (Model) is created, we can access our data objects using it.
Now and by leveraging self-object, we can do all the modifications we need using simple methods :
- addTask: takes a new task and adds it to the beginning of the tasks array by using
unshift
(Simple vanilla Js). - removeTask: takes a task and call the destroy method provided by MST, it removes a model element from the state tree (Child), and marks it as end-of-life so the element should not be used anymore.
- resetTasks: it calls the
applySnapshot
MST method, as i mentioned before, we can restore our data from a snapshot already created, so we can use this powerful method to reset our data by passing an empty object. - setFilter: a simple setter method.
In the same way we can apply some actions on the Task entity :
//Task.ts
import {getParent, types} from 'mobx-state-tree';
export const TaskModel = types
.model('Task')
.props({
id: types.identifier,
task: types.string,
isCompleted: types.optional(types.boolean, false),
creationDate: types.Date,
})
.actions(self => ({
markAsCompleted() {
self.isCompleted = !self.isCompleted;
},
delete() {
getParent(self, 2).removeTask(self);
},
}));
- markAsCompleted : a simple method to change the isCompleted value to mark it as completed or not.
- delete : the only responsable of deleting a Node in a tree is his parent, To find a parent with a Node we need to use
getParent
method provided by MST, it takes 2 arguments :
- First Argument : is the node for which you want to get the parent, the self object in our case.
- Second Argument : is an integer value that represents how many levels up in the tree to go to find the parent node. The default value is 1,which means it will return the immediate parent of the given Node.
In our case , the depth 1 is the Tasks Array , and depth 2 is the Store that holds theremoveTask
method.
Let's see now how we can filter our data and extract some statistics from it.
How to extract information from the store ?
In MobX State Tree (MST), a view is a way of querying and subscribing to the current state of the application's data model. It is essentially a declarative way of specifying what part of the data model you are interested in, and how it should be computed or derived from the underlying data.
Let's implement the views to count the number of pending tasks, completed tasks, today's tasks, and so on. see the code below :
//TasksStore.ts
import {types} from 'mobx-state-tree';
const TasksStore = types
.model('TasksStore')
.props({
tasks: types.array(TaskModel),
filter: types.optional(filterType, SHOW_ALL),
})
.views(self => ({
get pendingTasksCount() {
return self.tasks.filter(task => !task.isCompleted).length;
},
get completedTasksCount() {
return self.tasks.length - this.pendingTasksCount;
},
get filteredTasks() {
return self.tasks.filter(TODO_FILTERS[self.filter]);
},
}))
Now that we have the Store , the actions , the views ...etc , let's see now how to consume the Store's data in our UI.
How to connect the Store with the front?
To link our store with the UI, we need to create an instance of it, this can be easily done by calling .create()
on the TasksStore we defined earlier and passing an initiale state.
//TasksStore.ts
..
let _tasksStore: TasksStore;
export const useTasksStore = () => {
if (!_tasksStore) {
_tasksStore = TasksStore.create({
tasks: [],
});
}
return _tasksStore;
};
💡 We don't need to passe a value to filter since it is an optional.
Now the instance of our Store is created , we can use it everywhere in our application.
//TodoScreen.tsx
import {useTasksStore} from '../store/TasksStore'; //Importing the instance
export const TodoScreen = () => {
..
const tasksStore = useTasksStore();
..
return (
<View>
..
{tasksStore.filteredTasks.map(..)}
</View>
)
}
But how can the components update whenever we make a change on the Store?
MST provides an observer that help us track changes in the Store and update the components accordingly.
The power of this observer doesn't end there; the component will only re-render automatically if and only if there is an impact and change(s) on the data.
A superpower that we don't have when using Redux, the components will re-render when the reducer is affected by the action, whenever we have an impact or not. No way to control that.
//TodoScreen.tsx
import { observer } from "mobx-react-lite"
export const TodoScreen = observer(() => {
..
return (
..
)
})
Best Practices
Tracking the data using Snapshots :
To create a snapshot of the state tree in MobX, you can use the getSnapshot
function. It can be used each time you update your state to determine whether the changes have been reflected.This function returns a plain JavaScript object that represents the current state of the tree.
Here's an example of how you might use getSnapshot
to create a snapshot of a state tree:
//TodoScreen.tsx
import {useTasksStore} from '../store/TasksStore';
import {getSnapshot} from 'mobx-state-tree'; ``
export const TodoScreen = () => {
..
const tasksStore = useTasksStore();
console.log(getSnapshot(tasksStore))
..
return (
<View>
..
{tasksStore.filteredTasks.map(..)}
</View
)
}
/*
LOG {"filter": "show_all", "tasks": []}
*/
//After adding a Task
/*
LOG {"filter": "show_all", "tasks": [{"creationDate": 1672960782033, "id": "0.7376613848288301", "isCompleted": false, "task": "Develop Signin Features"}]}
*/
You can now go more deeper and save it inside a file, or maybe using the local storage and so on.
Now you know how to get and save a snapshot, how about restoring the data from a snapshot ?
Restoring the data from a snapshot :
applySnapshot
is a method in the MobX State Tree library that allows you to update the state of your MST store (Model) to a previous state, represented as a snapshot.
This can be useful for implementing features such as undo/redo or for restoring the state of your application from a previously saved snapshot..etc, See this exemple:
import {applySnapshot} from 'mobx-state-tree';
const restoreData = () => {
applySnapshot(tasksStore, {
filter: 'show_all',
tasks: [
{
creationDate: 1672960782033,
id: '0.5499213848288393',
isCompleted: true,
task: 'Going to gym',
},
{
creationDate: 1672960782033,
id: '0.7376613848288301',
isCompleted: false,
task: 'Develop Signin Features',
},
],
});
};
Using type at compile time :
When we defined the model , we write the proprieties along with types in order to perform type checks at the runtime , but how about the compile time ?
MST offers the Instance<typeof MODEL> method that help us to extract an instance of the type in order to use it when we define our variables and method's props, no need to create another TypeScript interface:
//TasksStore.ts
import {Instance} from 'mobx-state-tree';
interface TasksStoreType extends Instance<typeof TasksStore> {}
let _tasksStore: TasksStoreType;
export const useTasksStore = () => {
if (!_tasksStore) {
_tasksStore = TasksStore.create({
tasks: [],
});
}
return _tasksStore;
};
And now as you can see in the example below, the destructing is working as expected :
Conclusion
Almost all the prerequisites to get started using MobX State Tree for your next projects have been covered in this post, including the upsides, how to install it, how to setup your Store and some best practices that I suggest to adopt when you are using this library.
You can find the full code of this application in this Git repo : Full-Examples
Thank you for reading this article!