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
Top comments (3)
Hi, Great article Matt! One question, let's say I am getting user info from API response and user info has lots of details. So in that case I need to create a user model first with all the data types and attributes first, right? So my model will be very big. What if the API adds/deletes a new attribute from user info? I need to update my model always according to that right?
Thanks again for the nice intro!
Hi Saad,
Tough to get into that in a whirlwind five minute intro :-)
I'll visit things like that in future posts. But here are a few suggestions.
Take a look at mobx-state-tree.js.org/overview/types
For a key value grab bag, you can use types.map or if your dataset is totally wild west, you can use types.frozen.
I've hit on what you're up against. What I normally try to do is model as much as I can, and try to constrain the wild attributes into a separate grab bag.
Glad you enjoyed the article! I'll try to keep them coming.
Thank you so much for the quick response and helpful link. I will go through them. Hopefully will see you doing a series on this!