DEV Community

Matt Ruby
Matt Ruby

Posted on

Beginners guide to mobx-state-tree in 5 minutes or less

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) │ │           │ │           │
│           │ │           │ │           │
└───────────┘ └───────────┘ └───────────┘
Enter fullscreen mode Exit fullscreen mode

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),
});
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
    },
  }));
Enter fullscreen mode Exit fullscreen mode

Let's use that new action:

slide.selected // false
slide.setSelected(true) // calling the action
slide.selected // true
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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),
});
Enter fullscreen mode Exit fullscreen mode

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);
    },
  }));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 observers 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

Oldest comments (3)

Collapse
 
saadbashar profile image
Saad

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!

Collapse
 
mattruby profile image
Matt Ruby

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.

Collapse
 
saadbashar profile image
Saad

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!