Beginners guide to mobx-state-tree in 5 minutes or less
Matt Ruby
Posted on February 2, 2021
The problem we're trying to solve
Let's jump right in! We have a problem, we want to show our customers a simple image viewer.
We'll show a simple slideshow:
┌───────────────────────────────────────┐
│ │
│ │
│ Image 1 │
│ │
│ │
└───────────────────────────────────────┘
┌───────────┐ ┌───────────┐ ┌───────────┐
│ │ │ │ │ │
│ Image 1 │ │ Image 2 │ │ Image 3 │
│(selected) │ │ │ │ │
│ │ │ │ │ │
└───────────┘ └───────────┘ └───────────┘
The data model
In Mobx-State-Tree (MST) you're working with models. What is a model?
import { types } from "mobx-state-tree";
const Slide = types.model("Slide", {
id: types.identifier,
url: types.string,
description: types.string,
selected: types.optional(types.boolean, false),
});
This Slide
model gives us a blueprint for an observable slide. Here's an example of hydrating that model with data:
const slide = Slide.create({
id: "1",
url: "http://url_to_the_image/whatever.jpg",
description: "Grey cat",
});
Cool beans! We have a slide.
Here's your new slide serialized:
slide.toJSON()
{
id: "1",
url: "http://url_to_the_image/whatever.jpg",
description: "Grey cat",
selected: false, // cool, it defaulted to false
}
Now what? Well, not much. Models in MST are only editable via actions. What are actions you ask? Here's an example:
const Slide = types
.model("Slide", {
id: types.identifier,
url: types.string,
description: types.string,
selected: types.optional(types.boolean, false),
})
.actions((self) => ({
setSelected: (isSelected) => {
self.selected = isSelected;
},
}));
Let's use that new action:
slide.selected // false
slide.setSelected(true) // calling the action
slide.selected // true
Now we're able to modify our slide. Great! Much like a tree falling in the woods, does a modified slide change anything if no one is listening? I'll let you ponder that one while we add an observer. What's an observer you ask? Great question!
An observer is something that listens to changes within an observable. They're used to trigger side effects. Like updating your UI or printing something to the console.
If you were reading carefully above, you'll remember when I mentioned: "This Slide
model gives us a blueprint for an observable slide." If we're creating observables, it goes to reason that we can observe them. MST is built on mobx. Mobx makes observing changes easy. Observe :-)
import { autorun } from "mobx";
autorun(() => {
console.log('Slide is selected: ' + slide.selected)
})
// Slide is selected: false
slide.setSelected(true);
// Slide is selected: true
autorun
is a simple observer that will watch any observable that is used within it. It's also run once determine what it needs to watch.
There are many ways to observe observables via reactions.
If you're using React, there are already tools available to easily observe your models -- most notably, mobx-react-lite's observer() function. I'll show you an example of how that works near the end of this article.
Now you know how to create models, hydrate them with data, change their state and react to changes!
From here, we need to add another model that represents the collection of slides.
Collecting the slides into a slideshow
We have a slide, that's cool... But it's not enough. We need to turn that one slide into a slideshow. Here's a start:
const SlideShow = types.model("SlideShow", {
slides: types.array(Slide),
});
This is still not enough. We could show a slideshow at this point, but we couldn't interact with it. In addition, we have to do a little digging to find the selected slide. Let's first take care of finding the selected slide.
const SlideShow = types
.model("SlideShow", {
slides: types.array(Slide),
})
.views((self) => ({
get selectedSlide() {
return self.slides.find((slide) => slide.selected);
},
}));
selectedSlide
is a view. That view is observable just like any other field. One of the major tenets of mobx is that "Anything that can be derived from the application state, should be. Automatically." Views are how this is done.
Let's work on being able to select a slide. In order to do that, two things must happen. First, the currently selected slide should be de-selected. Second, the slide to select should be set as such.
There are a few ways to go about selecting a slide. We could call upon the parent SlideShow to toggle the selected states. The api would probably look something like this:
slideShow.setSelectedSlide("2") // pass the slide id to select
// OR
slideShow.setSelectedSlide(slideShow.slides[2]) // pass the slide
The bummer for me in this option is that you have to keep track of both the SlideShow and the slide wherever you want to trigger a selection. Chances are you'll have the slide handy that you'd like to select when it's clicked for example.
I'd prefer an api that looks more like this:
slide.select()
So, let's build that!
import { types, getParent } from "mobx-state-tree";
const Slide = types
.model("Slide", {
id: types.identifier,
url: types.string,
description: types.string,
selected: types.optional(types.boolean, false),
})
.actions((self) => ({
setSelected: (isSelected) => {
self.selected = isSelected
},
select: () => {
getParent(self, 2).selectedSlide.setSelected(false);
self.setSelected(true);
},
}));
const SlideShow = types
.model("SlideShow", {
slides: types.array(Slide),
})
.views((self) => ({
get selectedSlide() {
return self.slides.find((slide) => slide.selected);
},
}));
const slideShow = SlideShow.create({
slides: [
{
id: "1",
url: "http://url_to_the_image/grey.jpg",
description: "Grey cat",
selected: true,
},
{
id: "2",
url: "http://url_to_the_image/blue.jpg",
description: "Blue cat",
},
{
id: "3",
url: "http://url_to_the_image/yellow.jpg",
description: "Yellow cat",
},
],
});
slideShow.selectedSlide.description; // Grey cat
slideShow.slides[2].select();
slideShow.selectedSlide.description; // Yellow cat
And with that, we have a working, observable slideshow model! Not much of a UI... Let's fix that now.
Adding a UI
So that model is pretty terrific... But it's a little hard for most people to use right now. It's time to create a derivation of our data in the form of a UI.
Why did I call our UI a "derivation of our data"? Because it is :-)! The data model acts as the source of truth about the state of our app. The UI is just one of many potential derivations of that data. Analytics, debugging, native apps... Everyone wants a piece of the action.
Let's look at one very simple React based UI:
Here, I'm using observer
s from mobx-react to watch for changes in my data model. The observers are automatically optimized to only update when an observed piece of data changes. Not so important with this trivial example. But as applications grow, it becomes more important.
Well, that's all for now. Next time, I think we'll look at how to test our data model.
Until then, have fun out there! I know I am!
-Ruby
Posted on February 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.