Create Relationships in your Data with mobx-state-tree Using References and Identifiers

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

MST stores all data in a tree, where each piece of data lives at one unique location. In most state management solutions, this means you need to introduce weakly typed, primitive 'foreign keys' to refer to other parts of the state tree. But in MST you can leverage the concepts of references and identifiers to relate model instances to each other. MST will do the normalization behind the scenes for you.

In this lesson you will learn:

  • The use of types.identifier to uniquely identify an instance of a specific type
  • The use of types.reference to refer to data in another part of the tree, and with which you can interact with in another part of the tree
  • The use of types.maybe to make as ".. or null" type
  • Using types.late to create circular type definitions

Instructor: [00:00] We now have multiple users in our application and multiple wish lists, but an important feature is missing. It's not possible yet to draw lots and to see what gifts you should buy for somebody else, which is, of course, the best part of the whole thing.

[00:16] Let's introduce an action on our group. We call it drawLots. We need to be able to store whose lots you draw, who's the recipient of your gifts. That sounds like the recipient of a user is another user. That looks quite nice, but this won't work.

[00:35] The problem is that our data model is wrong. We model the user as having a recipient which is another user. However, the user, the recipient, isn't a user per se. It's just a reference to another user. If we would put a new user in there, that would be weird. Then the user would own another user, which is not appropriate here.

[00:57] On the other hand, if we assign a user to the recipient property, that means that our whole model tree is no longer a tree. That would make that user occur in two places in our state -- one, in the collection of our users, in the group over here, and it will also be stored as the recipient over here.

[01:17] Mobx-state-tree will definitely throw if you try to do something like this, because everybody should be uniquely contained. That guarantees that we can serialize with the snapshots, that patches can be properly located, et cetera.

[01:31] Instead of storing a user, we actually want to store a reference of the user. We could be storing its ID. We could change this to a type of string. This whole process of drawing lots is not trivial, I'm just going to paste in an implementation. Let's save this and see it in action.

[01:52] If we now draw the lots, we have recipients, but it's not the most readable. This is apparently the person with this ID. Probably we should be rendering the user's name here, but we just stored an ID. To get back the name, we could introduce a view function on the user, which returns the person we drew.

[02:14] For example, we call it other, and we just say in our parent -- which is the map place -- "Get me the person who's ID I'm using." Now we can simply say, "Does the user have another person?" Then we render his name. We created a small function to do a look-up, and now we see a nice name, Lisa.

[02:35] Now this works. In the model, we now have recipients, stored as string, which is basically the foreign key of the user we want to have here. Then, we created a view that looks up the parents. You can even imagine creating actions, where you can assign recipients the same way, and then actually store their ID. That would look like this.

[02:55] You can imagine this view and setter for each foreign reference you have becomes a bit boiler-plating. That is why mobx-state-tree has first-class support for this mechanism. My original problem was that we couldn't set the user here, because this wouldn't break that tree model.

[03:13] We can assign, as recipients, a reference to a user. A reference is a very special type, which does all the reference resolving for us in the background. Behind the scenes, it stores the ID of the user we're talking about. You don't have to do all the view and action fluff, you can lose that.

[03:33] To know whether your ID is of a type, you need to identify one attribute as being an identifier, which is a special type expressing, "This ID uniquely identifies my instance."

[03:46] Now, we can simplify our code, because we just directly assign recipients. Now, reading and writing uses first class, but still, in the background, if we would check out the snapshots, just the ID would be stored.

[04:02] We get one error, and that is the fact that it says, "Hey, user is not defined yet." That's because this constant in JavaScript is not yet assigned, while we refer it here. Basically, we have a recursive type definition, because the type of user needs to be known to construct the reference, which needs to be known to construct the user type.

[04:21] Luckily, there's a simple fix for it. We can defer knowing the type. We can simply say types.late, and return user from that. This signals only when you actually really needed to know the type. It then evaluates this expression. That fixes the issue that the user type is not know up front and that the type is circular. Now, we have a circular type definition without issues that the type cannot be resolved.

[04:47] Now the issue is that, when we start our state, we don't have a recipient, so the reference is empty. We need to express in our type system that we allow a reference to be empty. We can do that by using the maybe type again. A recipient is either null or it's a reference to a user.

[05:09] Now, in our component, we also can get rid of that whole other thing and we can just make user recipient here again. Now, if we have Homer, then we have a really nice reference to Lisa.

[05:21] Now, we can also improve our actual rendering a bit and actually show you what gifts we have to buy. We can just reuse the wish list view, and this time, we render the wish list of the recipient. Ideally, we should render this wish list read-only. After all, it shouldn't be edited by this user. If we now view Homer, then he can also see Lisa's list with the gifts he has to buy.

Gar Liu
Gar Liu
~ 4 years ago

Hello Michel. I understand what types.optional is but not too sure about types.maybe. What is the difference between the 2?

Michel Weststrate
Michel Weststrateinstructor
~ 4 years ago

optional means that the value can be left out of the snapshot, in which case it will fall back to the default. Maybe means that 'null' is itself is an acceptable value for that property. So x: types.optional(types.number, 3) means that "x" is always a number, and 3 if not provided. in contrast x: types.maybe(types.number) means that "x" might be a number, but it might also be "null"

Op do 29 mrt. 2018 om 18:15 schreef Gar notifications@egghead.io:

Gar Liu
Gar Liu
~ 4 years ago

Is

types.optional(types.number, null);

the same as

types.maybe(types.number);

Michel Weststrate
Michel Weststrateinstructor
~ 4 years ago

The the first is is an invalid type; null is not assignable to a number. So the default you pass to optional should be valid for the type you are trying to make optional

Op do 29 mrt. 2018 om 19:50 schreef Gar notifications@egghead.io:

Gar Liu
Gar Liu
~ 4 years ago

Ok. I get it. If i need to assign null then use maybe.

Maung San
Maung San
~ a year ago

Can you please show code snippet on how to test a model that has references to another model(or) has reference to itself? For example, the user model that is used in the video.

const User = types
    .model({
        id: types.identifier,
        name: types.string,
        gender: types.enumeration("gender", ["m", "f"]),
        wishList: types.optional(WishList, {}),
        recipient: types.maybe(types.reference(types.late(() => User))),
    })

Thanks in advance!