Skip to content

Instantly share code, notes, and snippets.

@thomasboyt
Created December 20, 2016 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasboyt/8cda9c533802a36ee6aac4559c4799b1 to your computer and use it in GitHub Desktop.
Save thomasboyt/8cda9c533802a36ee6aac4559c4799b1 to your computer and use it in GitHub Desktop.

Scattered Thoughts on MobX

It's been a while since I've blogged, which is what tends to happen when you start a new job. Manygolf development's been more-or-less paused - though I still want to get a new version out early next year - but I've managed to use my free time over the last month to start work on a new site called Jam Buds.

Jam Buds is a little site to help share music you like with your friends. It tries to solve a problem I have whenever I want to share music with people: I tend to post music to Twitter when I discover it, but most of the time, when people are actually reading my Twitter, they're not really in the mood to just go and spend three minutes listening to some song someone posted. Jam Buds is a site that gives you a dedicated place to find and share music (using your Twitter following list as a springboard to find people to share music with).

If all of that sounds familiar, well, that's good! I've never actually used This is My Jam, but I know people loved it, and I hope I can provide something people love a fraction as much. I definitely will never have the level of resources that were poured into that site (let alone the design acumen), but I'll do what I can.

I'm building Jam Buds with a stack that should be familiar to anyone who read about Manygolf's stack: a TypeScript & Express server, with a TypeScript & React frontend. However, unlike Manygolf, Jam Buds uses MobX instead of Redux to handle state. This has been a really interesting experiment, and I've been super happy with the results so far.

Why not Redux?

I've been writing apps with Redux for, more or less, two years, and before that was writing apps using other Flux-inspired libraries. I've been very happy with Redux in a lot of ways, especially when combined with Immutable.js. Being able to structure my state as a giant immutable tree is pretty damn cool.

What I haven't been super happy with is trying to write Redux apps with TypeScript. See, on the surface, there is nothing about Redux itself that doesn't play well with TypeScript. You can theoretically type-check every layer of a React+Redux application, and many new features in TypeScript have been built with improving type-checking these sorts of apps in mind.

Unfortunately, while Redux is now robustly type-checked, libraries like Immutable or updeep aren't. While TypeScript 2.1 brings new features that should hopefully improve Immutable's type-checking, this still requires a significant amount of boilerplate, and Immutable itself hasn't been updated to support these features.

TypeScript 2.1 does also support object rest/spread, so "vanilla" Redux reducers can be used, but I actually really hate writing "vanilla" Redux reducers. I find properly rest/spreading fields that are more than one or two layers deep becomes really tedious and error-prone, thus my love of Immutable.

In addition to concerns about TypeScript, there were other reasons why I wanted to try a new state management library. I have pretty loudly and constantly complained about the boilerplate Redux applications encourages, and wanted to try something like MobX that had fewer layers.

So what is MobX?

The shortest answer I can give this question is, admittedly, kind of glib: MobX falls somewhere in between Ember.js's route/controller layer and pre-Redux Flux implementations.

MobX's documentation has a solid one-page overview. Basically, you can make observable objects (which usually start out looking like Flux stores) containing your application state. You then set up your views to subscribe to changes on passed-in stores. When you update your observable objects, your views automatically update, through some kind of magic.

And, really, MobX looks a lot like Ember's object model, if you're familiar with that. It has observables, computed properties, and auto-run subscriptions on your state. But you might notice that, unlike Ember, you don't have to define anywhere what your computed properties or observers are actually dependent on.

That's because the key bit of magic that MobX has is that all of these things that "react" to changes only react when the data they actually access is changed. In other words, you can pass down a whole, giant store into a component, but the component will only rerender if the piece of the store it accesses changes.

In addition, MobX has other built-in patterns that make it easy to fall into well-optimized code. An explicit @action decorator around methods that update an object's state makes it trivial to batch a set of changes into a single transaction, ensuring your views only update once no matter how many observable attributes you synchronously update.

What advantages does MobX have over Redux?

Type-checking

The most obvious one is the one that got me off Redux in the first-place: MobX just uses vanilla ES6 classes, so everything is easily type-checked! It looks quite nice in practice, too:

import {observable} from 'mobx';
import {PublicUser} from '../../universal/resources';

export default class UserStore {
  @observable loadedUser: boolean = false;
  @observable loggedIn: boolean = false;
  @observable name: string | null = null;
  @observable userId: number | null = null;
  @observable following: PublicUser[] = [];

  // ...
}

There's no extra ceremony required, other than making fields observable.

Async Request State

I'm pretty happy with how async actions and state management looks in MobX, too. In MobX, actions live as methods on your observable objects:

import {observable, action} from 'mobx';
import {PublicUser} from '../../universal/resources';

import followUser from '../api/followUser';

export default class UserStore {
  // ...
  @observable following: PublicUser[] = [];

  // ...

  @action async followUser(userId: number) {
    const user = await followUser(userId);
    this.following.push(user);
  }
}

Now all a view component has to do is call this.props.userStore.followUser(id) somewhere, and everything updates nicely.

Of course; this particular example is lacking the error/loading state boilerplate I'm so fond of complaining about. It'd be easy enough to add an isLoading flag to this store, but the mobx-utils library has an interesting tool, fromPromise, that makes it very easy to add these states to a component.

Here's an example of fromPromise used in a store that fetches a user's playlist:

import {observable, action, computed} from 'mobx';
import {fromPromise} from 'mobx-utils';

import getPlaylist from '../api/getPlaylist';
import PlaylistEntry from './PlaylistEntry';

export default class PlaylistStore {
  @observable name: string;
  @observable userId: number;

  @observable items: PlaylistEntry[] = [];

  @action getPlaylist(name: string) {
    this.name = name;
  }

  @computed get itemsPromise() {
    return fromPromise(getPlaylist(this.name).then((resp) => {
      this.items = resp.tracks.map((track) => new PlaylistEntry(track, this));
      this.userId = resp.user.id;
    }));
  }
}

In my component, I just render like this:

import * as React from 'react';
import {observer, inject} from 'mobx-react';

import PlaylistStore from '../../stores/PlaylistStore'

interface Props {
  playlistStore?: PlaylistStore;
  params: any;  // from react-router
}

@inject((allStores) => ({
  playlistStore: allStores.playlistStore as PlaylistStore,
})) @observer
class Playlist extends React.Component<Props, {}> {
  componentWillMount() {
    const name: string = this.props.params.name;
    this.props.playlistStore!.getPlaylist(name);
  }

  // ...

  render() {
    const {itemsPromise} = this.props.playlistStore;

    return itemsPromise.case({
      pending: () => <div className="main-placeholder">Loading...</div>,
      rejected: () => <div className="main-placeholder">Error loading!</div>,
      fulfilled: () => this.renderLoaded(items)
    });
  }
}

For components that trigger requests as user actions (i.e. deleting an item in your playlist), I use the same pattern, except the observable is just kept as a field on the store.

Domain Objects

One very powerful feature of MobX is that it actually encourages you to make domain objects - that is, models - part of your application. That's right, in MobX, the dream of OOP is still alive (in a very carefully-architected manner!).

Domain objects, of course, just colocate a piece of state in your application, and actions that update that state. For example, in Jam Buds, a playlist entry is a domain object that exists within the playlist store. This means that, rather than having a top-level store that has actions like deleteEntry or likeEntry, the entry itself has delete and like actions. This makes it trivial to store request state as mentioned above without creating, say, a map of requests to the playlist entries they're updating.

This MobX docs page covers this recommended architecture pretty well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment