DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Data Engineering
  3. Data
  4. One Awkward Thing About MobX: Complex Models

One Awkward Thing About MobX: Complex Models

Read on to learn how to use MobX to implement domain logic and create models in your web app. But be warned, it might get a little awkward.

Swizec Teller user avatar by
Swizec Teller
·
May. 17, 17 · Tutorial
Like (2)
Save
Tweet
Share
6.00K Views

Join the DZone community and get the full member experience.

Join For Free

Let's take an example from my day job. An onboarding step for new tutors is a subject exam. It starts with this page:

In this case, the applicant has already flunked chemistry, we're not accepting physics, and they can still take a math exam. To build this, we need data about the current user, about the subjects, and how they relate to each other.

Basic API-Backed MobX Model

Building a basic model with MobX is straightforward once you discover extendObservable. It's a store that gets its properties from the API.

A User model might look like this:

class User {
    constructor(mainStore, id) {
        this.mainStore = mainStore;
        this.id = id;

        this.fetchFromAPI()
    }

    @computed get url() {
        return `/api/users/${id}`;
    }

    @action fetchFromAPI() {
        fetch(this.url)
            .then(response => response.json())
            .then(action('set-user-params', json => {
                extendObservable(json);
            }));
    }
}

The constructor takes a user id and a mainStore reference. Having that reference to a parent store is often useful when spelunking through your data model. I learned that the hard way.

We call fetchFromAPI to fetch the user data, use a url computed value for the URL, and naively use extendObservable to set all the API data as observables on our User model.

This approach embraces duck typing. There’s no advance declaration of what a user model looks like. No description of properties, nothing you can rely on just by looking at the code. You’re just gonna have to try to access a property and hope it works.

But when your model looks like this:

Do you really want to type all of that out? No, you don’t.

extendObservable is a convenient approach for these situations. It gives you infinite flexibility and makes properties deeply observable.

 You should be careful about undefineds, though. Until the API call goes through, anything beyond user. is undefined. That part is annoying.

Sidenote:

You can fake a maybe monad in MobX using when, like this:

when(() => user.subject_statuses,
        () => // do stuff with data)


Connecting Models Is Where Things Get Awkward

Let’s have a mainStore that looks a lot like the User model above. It may look like the User model, but it fetches a bunch of things about the environment, one of which is the current user’s id, another of which is a list of subjects.

const MainStore {
    @observable currentUser = null;

    constructor() {
        fetch('/api/environment_data')
            .then(response => response.json())
            .then(this.setData);
    }

    @action setData(json) {
        this.currentUser = new User(this, json.currentUser.id);
        delete json.currentUser;

        extendObservable(this, json);
    }
}

Whoa, did you see that? We fetched data, took the currentUser.id, then deleted currentUser before using extendObservable. What?!

The API returns an object with many properties. In our example, let’s assume it looks like this:

{
    currentUser: {
        id: N,
        // other stuff, maybe
    },
    subjects: [
        {id: N, name: 'Math'},
        {id: N, name: 'Chemistry'},
        // ...
    ]
}

We want to avoid writing a bunch of boilerplate, so we use the extendObservable trick to set everything. But we don’t want to overwrite our custom currentUser, which is a proper MobX model, not just an observable.

So we have to delete that part of the data before extending. Awkward.

And why do we want User to be more than just an observable? Because it’s going to need @computed values. We might re-fetch the model at random points or perform many actions that save stuff to the backend, change the frontend state, and so on.

Dealing with all of that in MainStore would get hairy, fast.

You’re right. From the perspective of this example, that’s a total YAGNI – ya ain’t gonna need it. But bear with me.

Add the Domain Logic, Increase the Awkward

Using the above MainStore, rendering a subject picker is straight forward. Loop through the observable list of subjects, render elements. If something changes, MobX takes care of it.

Not that the number or naming of subjects ever changes.

const SubjectPicker = inject('mainStore')(observer(({ mainStore }) => {
    if (mainStore.subjects) {
        return (
            <div>
                {mainStore.subjects.map((s, i) =>
                    <Subject subject={s} key={i} />
                 )}
            </div>
        );
    } else {
        return null;
    }
}));

Assume we put an instance of MainStore in as mainStore. That gives us global access via inject and cleans up our codebase.

A simplified component would look like this:

const Subject = observer(({ subject }) => (
    <div>
        <h3>{subject.name}</h3>
        {subject.userStatus.can_start_exam ? 'Go for it' : 'LoL Nope.'}
    </div>
));

Not as pretty as the screenshot, but all that HTML and styling is irrelevant right now, so I took it out. Ours renders the subject name in an h3 tag, and a string Go for it if you can take this exam or LoL Nope if you can’t.

Now, the trouble; can you see it?

It’s that subject.userStatus.can_start_exam bit. You see, we’ve found ourselves staring at a piece of data that lies between concepts.

Ask a subject if it’s startable and it needs to know who you’re asking about. Ask a user and it needs to know which subject you’re asking about. It smells like a computable value, but where?

Computables can’t take arguments.

So even though the User model has this data – stored in subject_statuses – we can’t put the computed value there. It can’t go in MainStore either because that, too, would require arguments.

“Dude, it’s called a method. Just use a method”, right?

Yes, methods and functions can take arguments. But MobX computeds are preferable because they automatically observe values, memoize their results, and so on. Cleaner code. Faster code.

We need a new Subject model that knows about currentUser. It looks like this:

class Subject {
    @observable currentUser = null;

    constructor(currentUser, data) {
        this.currentUser = currentUser;
        extendObservable(this, data);
    }

    @computed get userStatus() {
        return this.currentUser.subject_statuses
                            .find(s => this.id === s.subject_id);
    }
}

It takes a reference to currentUser and raw data about itself then uses extendObservable to set it all up. We’ve added a computed userStatus method that goes to currentUser and finds the correct data.

This part is awkward, too. A model that returns information from a different model? What?! 

I’m not sure. Maybe it’s dumb. Maybe I’m dumb. I think it makes sense.

Anyway, in MainStore, we now do something like this:

    @action setData(json) {
        this.currentUser = new User(this, json.currentUser.id);
        delete json.currentUser;

        this.subjects = json.subjects.map(
            s => new Subject(this.currentUser, s)
        );
        delete json.subjects;

        extendObservable(this, json);
    }

Not very elegant, is it? And I think the subjects array stopped being observable in the process, so we have to add it as an @observable property explicitly.

Conclusion

  1. Use extendObservable to load and create large models.
  2. Create new models when submodels need @actions or @computeds.
  3. Do the new Model then delete dance when that happens.

It would be nice if extendObservable could do this for us. I'm sure it's possible as long as model constructors are forced to follow a common pattern.

But maybe that exists already and I don't know about it? Would definitely be a fun open-source project. 

Data (computing)

Published at DZone with permission of Swizec Teller, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Spring Boot Docker Best Practices
  • Project Hygiene
  • How Observability Is Redefining Developer Roles
  • How To Convert HTML to PNG in Java

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: