Om No! Trouble in paradise with ClojureScript and React.js

3 min read Original article ↗

Now for the part that everyone is waiting for! The parts that caused so much unnecessary confusion, pain, and suffering.

The API. I went into this a lot more during the talk, but it honestly doesn't matter as much as the other reasons in the long-term, and had little bearing in why we switched away. It's error-prone, verbose, and incomplete, but these were problems during the learning curve. We could deal with it. Sure, it made things unnecessarily complex and harder to explain to new people, but we thought it was the price to pay for the beauty that is declarative rendering in ClojureScript. We wrote some macros, and just dealt with it.

When first starting out, we used quite a few anonymous functions as components. It turns out that they don't work too well. What happens is that a new component is created every time, and it re-mounts to the DOM without any error messages or warnings. If you're lucky and your component is slow enough, it will flicker to make it visually obvious that something terrible happened. This was never caused bugs after we found out (thanks to more macros), but I still consider it quite a downside that it isn't supported.

A similar problem is the lack of first-class components. We found wrapping components with other components to be a very useful DRY maintainable pattern. Om disagreed though: you can't pass them around and compose them.

And then there's cursors. We also had a lot of issues with these (that were in the talk, but the others are not as bad), but the biggest issue was that cursors are pointers. They're references to mutable state (the global app state) and lead to some really annoying problems. The data that a cursor points to may have changed, but the location where an async transact! will not have (this issue was primarily caused by vectors in the app state, and either having new elements added or the order of elements changed). This is especially annoying in a primarily asynchonous environment.

Components work best when they know as little data as possible. That way, unnecessary rerendering doesn't have to occur. Om is a very poor fit for this since: (1) app state is in a tree and you need knowledgeof the lowest common ancestor of all the data you need and (2) parents must depend on everything all their children depend on. This results in either (1) the same data in multiple places or (2) a performance hit. This problem was a killer. It was hard to be consistent. It was hard to be performant. We were about to implement a DAG that automatically reconstructs an app state tree by denormalizing ground-truth data when the data changes, but then we discovered that is exactly how Reagent works.