Over the past couple of months I’ve been working on a web app, writing it in Clojurescript and using David Nolen’s Om framework. I may end up writing about some detailed aspects of Om, but for now I would like to talk a little bit about what it is like to build an Om app. This isn’t meant to be a tutorial at all, or an example; I just want to give a sense of what it feels like, and what some of the harder things to grasp are.

(If you are unfamiliar with Om, I recommend reading David Nolen’s blogpost, and listening to this Javascript Jabber episode. Or watching David Nolen’s talk at the last Clojure/West.)

My previous experience with Javascript frameworks was mostly with Backbone.js. And before Backbone existed, at least publicly, I already could sense that I needed some kind of tool to abstract the relationships between data (what would eventually become models in most frameworks) and the DOM. On one project, I wrote a kind of abstraction layer that tried to separate the data from the view. Everything was based on anonymous functions that got passed around. It worked, but when I came back four years later to debug it, I realized that it was completely incomprehensible. The only consolation I got was that because data and presentation were more or less separate, it only took about a day and a half to shoehorn it into Backbone.

Anyway, I digress…​

React and Om (from here on out I will mostly just say Om, but the two are obviously intertwined), change all of this. In my opinion, in fact, Om coupled with Clojure’s core.async, basically renders the MVC terminology obsolete. Not that we don’t have models, views and controllers anymore; it’s just that the layering of the three is no longer relevant. I would argue that in Backbone at least, the V and the C are already blended together, or that what Backbone calls a View is really a Controller, and that the DOM is in fact the View. Other frameworks handle this differently, and people have talked about MVVM as well. From what I know about other, more recent frameworks like AngularJS and Ember, they seem to be (huge) improvements over what Backbone started, but that they don’t really change the way we think about all of this.

Om (and React) are definitely view-centric. However, I would argue, for the time being anyway, that Om (with core.async) determines many other aspects of the process in a way that traditional Model or Controller layers are no longer relevant. In other words, this is not just a View layer is search of an M and a C. It is a different way of thinking about the whole process. And because it is so different I thought it would be worth going over some of the fundamental ideas, to perhaps alleviate some of the disorienting strangeness that we run into when we learn new patterns.

Lifecycle

If Om is so different, it is because the abstractions the developer has to deal with are rather different from what we are used to. And the React component lifecycle is the abstraction that is probably the hardest to understand, as well as being the abstraction that you have to deal with constantly.

By lifecycle, they mean the stages of how a given thing (a component) appears in the DOM, changes and perhaps disappears from the DOM. Every framework tries to take care of certain aspects of the task at hand so that we don’t have to think about them anymore. A big part of what Om does as a framework is moving your data through these different states.

Understanding what is happening here is therefore crucial, but difficult, especially if you, like me, don’t have any direct experience with React. And of course part of the point of using a library like Om is to not have to know React that well to get going.

So here is the complete lifecycle:

  • IInitState

  • IWillMount

  • IDidMount

  • IShouldUpdate

  • IWillReceiveProps

  • IWillUpdate

  • IDidUpdate

  • IRender

  • IRenderState

  • IDisplayName

  • IWillUnmount

These protocols are implemented by functions in Om components. A component must implement one of the rendering protocols, the rest are optional.

This list makes things look complicated, but we can actually pare this list down considerably. For example IRender and IRenderState are conceptually identical; IShouldUpdate is something that Om takes care of by itself, but could be an issue in pure React; IDisplayName exists really for debugging only.

With a little simplification, we really have four or five concepts to worry about. The basic lifecycle starts with an initial state, ie. IIinitState. When our component comes to life, it first needs to be mounted into the DOM. Hence: IWillMount, IDidMount and IWillUnmount, when the component is finally destroyed.

Rendering is the heart of this process, at least from the programmer’s point of view, because that is where we build the HTML that the user will eventually see. An important difference with other frameworks is that rendering happens a lot. For an animation, like moving a <div> from left to right, the IRenderState function might be called several times a second. Sometimes it is better to think of your app like a movie (a cartoon, I guess) rather than a static set of objects sitting around waiting for events to happen. And if you are thinking: “that must be horribly slow”…​ well, it isn’t, thanks to React’s virtual DOM that efficiently projects changes out into the real DOM.

So, back to the lifecycle, IRenderState basically gets called whenever something changes. Besides mounting and unmounting a component, there are also the “change” phases: IShouldUpdate (which we ignore, because in Om the components “know” if they should update or not), IWillReceiveProps, IWillUpdate, and IDidUpdate. The will and did parts are fairly simple: do we need to pre-process that new data coming in before updating (and rendering)? After updating, is there something we need to do, like grabbing the DOM node that might have changed, to get its dimensions, for example? They work like hooks in other systems.

I like to think of this as a river of data flowing down towards the virtual DOM. These protocols and their corresponding functions are how we guide that data to the right place and mold it into the right form.

And so what about the data?

React (and Om) are based on the idea of one-way data binding. Change happens in your data somehow, and the components in the app translate that into a new representation in the DOM. This is why the conceptual model is so appropriate for a functional programming approach, and so appropriate for Clojurescript with its immutable data structures: in a very broad sense, there is data on one side, then a series of functions that transform it, and finally DOM output on the other side. This gets us away from the traditional UI model where lots of little pieces talk to each other all the time, and state is intertwined (or “complected”) with the interface itself.

Of course, the details are a little bit more complicated than a pure data in, DOM out model, partly because we do need information from the DOM to work its way back up the river.

Om (and React) have two kinds of data: application state (which React calls “props”) and component state. This is an area that trips people up because it is not always clear what kind of data should go in what kind of state. More on that in a second.

Application state

In Om, all application state is generally contained in a single atom that has a tree structure composed of maps and vectors. Om uses something called a cursor to access individual parts of this tree. David Nolen has compared cursors to Clojure zippers. The interface to cursors is nohwere near as elaborate as zippers, but the idea is indeed similar: keep a path to wherever the data you are interested in is located.

For example, if you have a menu with a submenu in your app, you might reference it like this: [:main-menu :sub-menu].

The reason this is important is that most components will only have access to one or a possibly a few parts of the overall tree. In a more traditional javascript framework, we would say that some part of the View was “listening” to that part of the model. This would not be accurate with Om (or React), where it makes more sense to say that changes are being pushed out from the application state (the atom) to the view and ultimately to the DOM.

It’s worth noting that, in Om, both application state and components tend to be organized in a tree-like manner: components branch out into sub-components just like application state does. This pattern works well with the top down, data-pouring-through-the-ap approach.

Component state

The other part of the state picture is component local state. What should be local is sometimes a tricky question, but as a starting point, it might be helpful to think that component state tends to be more related to the mechanics of making the component work. For instance, if two components need to communicate with each other via a core.async channel, the endpoints of the channel would belong to each component’s local state. The other classic example is an animation where an offset is being incremented on each render; that offset doesn’t need to belong to the app state. It is just a “mechanical” part of making the component do what it should.

Back to application state, with an example

Application state, on the other hand, deals with what you are trying to represent. This can still be tricky, depending on what your definition of “what” is…​

In the project I’m currently working on, most of the actual content can be either in French or in Latin, and the user can choose to toggle between the two languages inside a given component. So at first, I thought this sounded like component state, since it was a question of choosing two different representations of the same time, and because all that data was already available to the component.

This quickly started to break down though, because pretty soon I wanted components to behave differently depending on what language was displayed by nearby components. I started setting up channels to tell the neighbors about changes to language state and everything started to get complicated and spaghetti-ish. I finally realized that my mental distinction between component state and application state needed some adjusting.

It turns out that the French/Latin language choice is really part of what the app is supposed to be showing. It isn’t an implementation detail, so it goes into the app state.

Earlier, I mentioned a menu and a sub-menu as being part of application state. In some circumstances, we could imagine that the contents of those menus might be derived from other information in the app. A menu isn’t so much a “thing” to represent as a tool within your application. However, since it is an entity that is part of what your are trying to show, it probably deserves its own piece of app state real estate. Whether a collapsing menu is visible or not might, on the other hand, be a suitable candidate for component state…​

At any rate, this isn’t meant to be a complete discussion of the topic, but just enough to give you an idea of how our thinking has to change when using Om.

Back upstream

React is supposed to be fast, and Om possibly even faster. But that implies interaction, and so far I’ve been describing a great river of data that lands in the DOM. How does change occur?

The simplest case would be a list item with a button that adds a star to the item when we click it. The list item would be a component, with the button part of the same component. The button would have a handler that would look a little like this, but not quite:

(defn star-handler [ev]
      (.preventDefault ev)
      (om/transact! list-item (fn [li] (assoc li :star :true))))

list-item refers to the part of the application state that is at the origin of this particular list item. om/transact! takes a function that operates on list-item, thus returning a new version with :star set to true.

What is nice here, is that application state gets changed immediately. If our render function is set up correctly, it will render a star now that :star is true. If there is a star counter somewhere else in the app, that keeps track of how many items are starred, it would be incremented immediately. Without any even listeners, except for the one listening for the click itself, these changes can be reflected everywhere in the app.

Now, this is probably the simplest possible case, because the state change concerns the item itself. If we wanted to remove the list item, instead of modifying it, we would have to do that from the parent component, or, to be more precise, the “owner” of the component. Unlike the DOM, “ownees” can’t access or manipulate their owners. Data just flows one way. So if we need to communicate with the parent or owner, to tell it to remove a child element, we can establish a core.async channel to tell it to call om/transact!, and everything will just work out. core.async allows us to jump back up the tree when we need to.

The same thing holds for new data coming in from the server. It goes into a channel and gets directly integrated into application state, and whatever changes need to be made can just flow back out through the rendering functions.

This is also why I was saying that Om (and React) are really much more than a View layer: Om has its own state management in the form of the state atom and cursors. It isn’t quite a model layer, but anything missing, like server sync, ultimately can just be added on as needed. The same is true of what would be the Controller: to the extent possible, you just want to write functions that react to input and modify data. In other words, while Om doesn’t provide everything you need to build a complete app, it is more than just a brick in a bigger edifice, because it imposes an architectural model that determines how you set up your “models”, your “controllers”. That is, to the extent that it is still relevant to talk about models and controllers in this context.

Next time, or maybe the time after that, I think I’ll talk about some of the things that are indeed somewhat difficult with Om.

Comments