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.
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.
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:
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
IShouldUpdate is something that Om takes
care of by itself, but could be an issue in pure React;
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
IIinitState. When our component comes to life, it first
needs to be mounted into the DOM. Hence:
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
So, back to the lifecycle,
IRenderState basically gets called
whenever something changes. Besides mounting and unmounting a
component, there are also the “change” phases:
(which we ignore, because in Om the components “know” if they should
update or not),
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.
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.
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.
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.
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
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
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
core.async channel to tell it to call
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.