A Redux Enthusiast tried MobX

Adam Rackis
9 min readJul 30, 2016

--

EDIT

Since writing this post, I’ve changed my opinion pretty sharply on MobX. See https://twitter.com/AdamRackis/status/775706291259908096

/EDIT

I’ve been coding with Redux (and React) for the better part of a year. I recently tried MobX, and decided to write up a few words on my experience, which overall was good, though not great.

I’ll provide a brief introduction to Redux, to help give context and contrast to MobX, then I’ll show how some use cases fared with both.

This post is far too long, and I lack the writing talent to make it shorter; I apologize, dear reader.

I’m not punching a strawman

If it looks like I chose these use cases to make MobX look bad, I swear that’s not the case. I’ve been poking around at this little side project of mine for some time now, and I just sought to implement some of the more basic use cases therefrom. I honestly expected things to go much better. The problem was not MobX; the problem was that I had forgotten how much Redux made me dislike Object Oriented Programming.

Redux in a nutshell

Redux stores your entire application state in one big, immutable object. Whenever your application state changes, a brand new object is created by your reducer. The format of a reducer is

const reducer = (state = initialState, action) => newState;

That’s it. Of course that reducer will quickly get unwieldy, but since it’s just a function, you can break it apart, and have your reducer call other reducers; this is called reducer composition. If any of this is new to you, stop and read the Redux docs. They’re short, clear, and beautifully written; I’ll wait.

Normalizing your data

I’ve previously written about the need to normalize your data in Redux. This means that you don’t store, say, your todo items in a flat array, but rather in an object hash, ie

let taskHash = {
1: { _id: 1, name: 'task 1' },
2: { _id: 2, name: 'task 2' }
};

Of course React can’t do many useful things with an object hash, so you shape the normalized data for consumption in React with selectors. Selectors are plain old JavaScript functions which take the state in, and return something else for React to work with. The selectors have access to your entire Redux state, not just the individual reducer it may be associated with, so shaping, stacking and combining your data from disparate parts of your state is always straightforward. Don’t worry, examples are coming.

What’s the catch

Boilerplate. There’s a lot of it. You have to type a lot more to do the same amount of work, and the code will likely be very different compared to what you’re used to.

Give it a chance though. That boilerplate is buying you an idiom that’s as clear and extensible as anything I’ve seen.

MobX in a nutshell

Store your data in objects. Apply decorators to class properties and React components, and mutate your data as needed; everything will just work.

Examples

I’ve been working on a book tracking web application on the side. It’s mainly for fun; I love to read, and it’s an interesting way to try out things like React and Redux.

Each book has a collection of subjects, and tags. The subjects are hierarchical, meaning American History could be a child of History, and so on. And of course the books’ subject and tag collections are normalized; they store subject and tag _ids, and so application code is responsible for associating a subject’s _id to the actual subject.

The hierarchical subjects are stored in materialized paths, which means each subject has a string path that stores its hierarchy. A top level subject of history would have a path of null, American history would have a path of ,history, Civil War could have a path of ,history,american history, and so on (except there’d be _ids between the commas, instead of names).

Example 1 Redux — loading books

When the load books ajax requests returns, the action looks like

case LOAD_BOOKS_RESULTS:
return Object.assign({}, state, {
loading: false,
booksHash: createBooksHash(action.books)
});
//along withfunction createBooksHash(booksArr){
let result = {};
booksArr.forEach(book => {
if (!book.subjects){
book.subjects = [];
}
if (!book.tags){
book.tags = [];
}
result[book._id] = book
});
return result;
}

that’s the reducer. We stack the books into a simple object hash, while cleaning up some potentially unclean data from Mongo. Then later we need a selector, to turn that hash into a useful array for React to display. The relevant code looks like this

function adjustBooksForDisplay(booksHash, subjectsHash, tagHash){
let books = Object.keys(booksHash).map(_id => booksHash[_id]);
books.forEach(b => {
b.subjectObjects =
b.subjects.map(s => subjectsHash[s]).filter(s => s);
b.tagObjects = b.tags.map(s => tagHash[s]).filter(s => s);
b.authors = b.authors || [];

let d = new Date(+b.dateAdded);
b.dateAddedDisplay =
`${(d.getMonth()+1)}/${d.getDate()}/${d.getFullYear()}`;
});
return books;
}

Note that adjustBooksForDisplay takes in the booksHash from the books reducer above, as well as the analogous subject hash, and tagHash from their respective reducers. As I said, selectors have access to the entire Redux state; you never have to worry about injecting dependencies, or any other OOP idiom for sharing data. The whole application state is in one place: just take what you need.

While this code likely seems a bit bloated to those new to Redux, having your data stored in a single, normalized state provides a degree of freedom I’ve never seen, for reasons I hope will be clear in a bit.

Books can of course be selected (in order to change their subjects or tags). To keep track of which books are selected, the book reducer keeps a hash of selected books, and the reducer action for selecting one looks like this (formatting for space as best I can)

case TOGGLE_SELECT_BOOK:
return Object.assign({}, state, {
selectedBooks: { ...state.selectedBooks,
[action._id]: !state.selectedBooks[action._id] } });

Example 1 MobX

Things are (ostensibly) more simple with MobX. Here’s a nice Book object.

class Book {
@observable selected = false;
toggle = () => this.selected = !this.selected;

constructor(book){
Object.assign(this, book);
this.subjectObjects =
this.subjects.map(_id => ({ _id, name: 'soon' }));
this.tagObjects =
this.tags.map(_id => ({ _id, name: 'soon' }));
this.authors = this.authors || [];

let d = new Date(+this.dateAdded);
this.dateAddedDisplay =
`${(d.getMonth()+1)}/${d.getDate()}/${d.getFullYear()}`;
}
}

along with a simple book loader class

class BookLoader {
@observable books = []
load(bookSearch = {}, publicUserId = ''){
Promise.resolve(ajaxUtil.get('/book/searchBooks', {
/*
a bunch of search props elided for space
*/
})).then(resp => {
this.books =
resp.results.map(rawBook => new Book(rawBook));
});
}
}

(the ajax call for Redux was not shown — it just dispatched the appropriate Redux action with the results).

Hey what about the subjects and tags

Yeah I just stubbed those out

this.subjectObjects = (this.subjects || [])
.map(_id => ({ _id, name: 'soon' }));
this.tagObjects = (this.tags || [])
.map(_id => ({ _id, name: 'soon' }));

I figured there’d be a similar subjectLoader object, and that this object would be passed to the bookLoader, and used to look up the proper subjects by id.

Looked up how, or maybe normalized data is more useful than we thought?

Remember the Redux code that mapped a book’s subject ids to subject objects? Finding a subject was trivial since the data was already normalized; it was just an object property lookup.

b.subjectObjects = b.subjects.map(s => subjectsHash[s])

If we’re storing our data un-normalized, in arrays, how would the lookup go in MobX? We don’t want to make an O(N) call to Array.find each time, so I assume the subjectLoader would need something like

@computed get subjectHash(){
return this.subjects
.reduce((hash, s) => (hash[s._id] = s, hash), {});
}

the bulk of that getter is literally copy pasted from my Redux reducer. I think it says something that normalized data is still essential even with an object model.

So let’s stop assuming what we’ll need and actually write the subject loading code.

Example 2 — Stacking the subjects in Redux

I’ve already written about managing these hierarchical subjects with Redux here. The short of it is that the subjects are stored in a hash (surprise surprise). The selector stacks them based on their path values with a basic regex. Best of all, when one subject has its parent changed, the ajax result returns the new paths for all modified subjects (if I change your path, all your descendants’ paths will change, too) — the subject hash is merged with the new data, and the selector re-stacks. Automatically.

Trying to stack them in MobX

Here’s my subject class (formatting for space as best I can)

class Subject{
_id = '';
@observable name = '';
@observable path = '';
@observable children = [];
@computed get childLevel(){
return !this.path
? 0
: (this.path.match(/\,/g) || []).length - 1;
};

constructor(subject){
Object.assign(this, subject);
}
}

And of course a SubjectLoader that loads the subjects

Promise.resolve(ajaxUtil.get('/subject/all', { })).then(resp => {
this.subjects = resp.results.map(s => new Subject(s));
});

So now I have a linear array of all my subjects. But how do I stack them? My first attempt was this

@observable subjects = []
@computed get stackedSubjects(){
this.subjects.forEach(s => {
s.children = this.subjects.filter(sc =>
new RegExp(`,${s._id},$`).test(sc.path)
);
});
return this.subjects.filter(s => s.path == null);
}

which error’d out

Error: [mobx] Invariant failed: It is not allowed to change the state when a computed value or transformer is being evaluated. Use ‘autorun’ to create reactive functions with side-effects

And no, autorun won’t work here. From the MobX docs:

mobx.autorun can be used in those cases where you want to create a reactive function that will never have observers itself.

I do need these stacked subjects to have observers.

You may be wondering why I don’t just stack the subjects in the ajax callback, and fill children arrays there — that would work, but changing the path value of any subject would no longer automatically re-stack the parent hierarchy, since a subject’s children array would not depend on its path (my attempted computed stackedSubjects does depend on each subject’s path since they’re referenced inside the property getter).

There’s an old saying that any problem in computer science can be solved by adding a level of indirection (except the problem of too many levels of indirection). One solution here would be to add such a layer which aggregates the subjects into stacks in a way that comports with MobX better. Or I could just stack the subjects in the ajax callback, as I mentioned before, and then, whenever any subject’s parents change, just fire off some sort of callback or event emitter, and manually re-stack.

After originally posting this piece, the MobX creator, Michael, tweeted me (he’s an incredibly nice guy) suggesting that I either use the paths to initially stack the children appropriately, and then (essentially) forget about paths; if the user moves a subject to a new parent, then just … move the subject to a new children collection. An interesting idea, and it would work, although I would then need to keep track of each subjects’ parent, as well as children. He also suggested I could derive the children, but more effectively than my first attempt. I went with that.

I also decided to store my subjects @observable normalized, as a hash (Michael helpfully pointed out that storing un-normalized data is more a norm, than a law). So then the subject @computed de-normalizes while stacking (the code is all at the end, in a gist — feel free to scroll if you want to see it now). It’s generally considered an anti-pattern to create side effects in a getter, so rather than mutate properties (ie the non-observable children) in the getter (which should not have error’d out), I created fresh subject objects with the MobX toJS utility. Of course this strips my prototype-link, so any prototype methods on my subject model are now gone (Subject has none, though). I also have to manually re-add computed properties, like childLevel, since toJS discards them.

A complete gist can be found at the end. I also combined bookLoader and subjectLoader into a single store object.

That books’ subject lookup from example 1

With this in place, I also re-visited example 1, of getting each book’s subject _id array morphed into an array of useful subject objects. My solution was to store an observable array of the actual books, and then make a computed from that, mapping to an array of books with the subject objects — this also uses toJS, like the subjects, and strips my prototype link: if toggle were a class method, instead of instance property, it wouldn’t work.

I could have also side stepped this by exposing the initial subject load in a promise, and then block on that in the load books callback, and then associate the right subjects to each book right in the ajax callback, and have the book model contain proper subject objects ab initio.

That’s actually the solution I‘ll probably use if I keep going on this: if nothing else, re-creating all those objects is just wasteful.

You talk too much; what are the takeaways?

OOP is hard, but not in the rewarding way. Getting objects to interact smoothly so you can just get stuff done is not easy. Some people love it. For those people, MobX will be like manna from heaven. And even if you don’t love OOP, if you hate boilerplate and functional programming more, then yeah, MobX is probably for you, and I won’t try to convince you otherwise. I imagine another huge use case for MobX will be upgrading old legacy code to React. MobX would probably help Knockout code get transformed over to React.

The Redux idiom fits me better, so I’ll likely stick with that. You should use whatever feels best for you, and don’t let any thought leaders here on Medium, or anywhere convince you one way or the other. Try them both and see what you like best. Dan Abramov can be credited with one of my favorite quotes: “be liberal with what you learn, and conservative with what you use.”

And here’s that gist I promised before

--

--