About 2 months ago, I gave a surprisingly well-received talk at the SF Clojurescript Meetup on the downsides of Om (I know, I expected to get lynched as well). Since then, I've been asked quite a few times for my slides and thoughts on the matter, so I figured that I could be DRY and write-up something to point people to (for that reason, I've also added a FAQ at the end).
We had a 5k+ LOC, fairly complex app (mostly logic and not just presentational stuff) in 100% Om, and we had been working on it for months. We were all past the learning curve and knew how to deal with Om, loved it, and really wanted to make things work. I especially wanted to make things work with Om because the team trusted me to make the call on which framework to use. Eventually, we just hit a wall with it. We made a switch to Reagent instead, and could not have been happier.
What Will Be Missed
There were some really nice parts about Om though, especially compared to other front-end frameworks, but compared to Reagent, there was only one thing: debugging with a global atom (sure it's possible with Reagent, but not as idiomatic). If you've never tried it, I'd recommend trying it (no Om app required, just put all your state in one)!
What Might Be Missed
There is a lot more that is really cool about Om, just that these things were things that we thought were more useful that it turned out to be in practice, or just not useful for what we were doing.
The first of which is global undo. It sounded amazing when people were talking about optimistically responding to users, and simply rolling-back when if an error occurs. That didn't really work for what we were doing, since other changes to the state might have occured in between, which made practical undo pretty complex. It seems much better suited for side-effect-less applications (graphical editors, etc.), and even then, local undo might be better than global undo.
The next of which is listening to deltas, which is pretty niche, but if you do need it, then you're in luck with Om!
The last good thing we didn't miss, which very much surprised us, was the community. There are lots of incredible people in the ClojureScript community who use Om, and their help was invaluable in getting as far as we did, but Reagent was just much easier to deal with.
What Won't Be Missed
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.
(I'll keep this short since this should be about Om.)
Reagent has just been amazing. It (mostly) follows the principle of least surprise, all of the Clojure-y goodness works (anonymous components, state in closures, components as data), and the data model just works better (from a performance/consistency point of view, at least where data is shared between components).
Just a quick random story. Right after we made the transition to Reagent, we had a new guy starting and a co-worker who has never seen Reagent code before was assigned as his mentor. This co-worker was able to teach the new codebase to the new guy perfectly. I can't imagine a better recommendation for any library than this.
I cannot think of a scenario that I would rather use Om than Reagent. Especially not for code to be maintained by more than just myself. Especially not in a complex app (from the data perspective). Especially not when performance is critical. I believe that the co-workers who experienced the Om phase feel the same way.
From the outside, Om like like it takes advantage of all the Clojure-y goodness, but it turns out that you lose a lot of it. Reagent solves every problem with Om we've had, and more, and it's been an absolute pleasure to work with. Try it, you won't be disappointed.
How hard was the switch?
Incredibly easy. We used sablono with Om, which made most of the presentation stuff almost "just work" with Reagent. There was a good amount of work that needed to be done for the logic of where all the state is kept and how it's accessed, but I saw that as more of cleaning up technical debt when the app state didn't have to be structured as an explicit tree.
Are you worried about how few people use Reagent?
I've actually been surprised by the number of people who talk to me about it. I wouldn't be suprised if there were almost as many people who use Reagent as Om, though they tend to be much less vocal about it.
Reagent seems unmainted. Are you worried?
What about all these reusable components and the rest of the ecosystem that are being built for Om? Won't you miss those?
I've personally never used them, so I can't comment on their quality.