A slice of React, Clojurescript and Om
Update 2014-01-16
Since I wrote this article, Om has undergone a few API changes. For consistency I have kept the original code in this article and put an updated version, using Om 0.1.6, that you can find here.
Also note that you must use a compatible Clojurescript version (0.0-2138) for Om to work – this was something I missed which gave me really weird errors when using lookups on the cursors.
React
React has sparked a lot of interest in the Clojure community lately (and perhaps, hopefully the other way around as well), and for good reasons. At the very core, React lets you build up your DOM representation in a functional fashion by composing pure functions and you have a simple building block for everything: React components.
“React components implement a
render()
method that takes input data and returns what to display.”
David Nolen’s Om library
About a week ago, David Nolen (@swannodette) presented a small Clojurescript library on top of React in a post on his blog.
The core idea in Om is to simplify idiomatic state management with Clojurescript’s immutable datastructures, while still getting all the performance (and more), as well as adding some syntactic sugar.
Where’s the code already?
I going to take a piece of the React tutorial and present in first Javascript, then Clojurescript and lastly using Om. Hopefully this will let us discover how plain Clojurescript interacts with React and inform us on some of the design decisions in Om.
var CommentBox = React.createClass({
render: function() {
return React.DOM.div({className: "commentBox"},
"Hello, world! I am a CommentBox");
}
});
React.renderComponent(
CommentBox(),
document.getElementById('content')
);
This is the very first code example from the React tutorial. It creates a very simple React component, consisting of a single div tag. As you can see, I have used the pure Javascript version. Let’s pickup the pace.
var comments = [
{author: "Pete Hunt", text: "This is a comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
var Comment = React.createClass({
render: function() {
return React.DOM.div({className: "comment"},
React.DOM.h2({className: "commentAuthor"}, this.props.author),
React.DOM.span(null, this.props.text)
);
}
});
var CommentList = React.createClass({
render: function() {
var comment_nodes = [], i;
for (i = 0; i < this.props.comments.length; i++) {
var comment = this.props.comments[i];
comment_nodes.push(Comment({author: comment.author,
text: comment.text}));
}
return React.DOM.div({className: "commentList"}, comment_nodes);
}
});
var CommentBox = React.createClass({
render: function() {
return React.DOM.div({className: "commentBox"},
React.DOM.h1(null, "Comments"),
CommentList({comments: this.props.comments})
);
}
});
React.renderComponent(
CommentBox({comments: comments}),
document.getElementById('content')
);
If you haven’t already read and worked through the React tutorial, now is a good time.
We have three React components. The top level CommentBox
that contains a CommentList
, which in turn is made up of a sequence of Comment
’s. The data is inserted at the top level and is passed down to the children.
“React is all about data flow between components. You can think of it as one way data binding (from parent to child).” – @floydophone on Reddit
Let us now translate this code directly into Clojurescript:
(def comments-data [{:author "Pete Hunt" :text "This is a comment."}
{:author "Jordan Walke" :text "This is *another* coment"}])
(def Comment
(js/React.createClass
#js {:render (fn []
(this-as this
(let [comment (.. this -props -comment)]
(js/React.DOM.div
#js {:className "comment"}
(js/React.DOM.h2 nil (:author comment))
(js/React.DOM.span nil (:text comment))))))}))
(def CommentList
(js/React.createClass
#js {:render (fn []
(this-as this
(js/React.DOM.div
#js {:className "commentList"}
(into-array
(map #(Comment #js {:comment %})
(.. this -props -comments))))))}))
(def CommentBox
(js/React.createClass
#js {:render (fn []
(this-as this
(js/React.DOM.div
#js {:className "commentBox"}
(js/React.DOM.h1 nil "Comments")
(CommentList #js {:comments
(.. this -props -comments)}))))}))
(js/React.renderComponent
(CommentBox #js {:comments comments-data})
(.getElementById js/document "content"))
There are two things that stands out in this piece of code. First, we have to juggle around a lot with Javascript object literals, and keep track of where we’re using Clojurescript types.
Secondly, in Clojure we’re used to the idea of having all application state in one place, wrapped in an atom, so breaking up the data into pieces and pass around isn’t very idiomatic.
Perhaps unsurprisingly, Om addresses both these issues. Instead of passing around the actual (pieces of) data, an atom is passed together with a path, pointing to the part of the state belonging to a specific component.
Further on, Om provides convenient functions for defining components (om/component
) and building components from application state (om/build
). At last, here is the Om version of our code:
(defn comment [{:keys [author text]} c]
(om/component
(dom/div #js {:className "comment"}
(dom/h2 nil author)
(dom/span nil text))))
(defn comment-list [app]
(om/component
(dom/div nil
(dom/div #js {:className "commentList"}
(into-array (map #(om/build comment app
{:path [%]})
(range (count app))))))))
(defn comment-box [app]
(om/component
(dom/div #js {:className "commentBox"}
(dom/h1 nil "Comment")
(om/build comment-list app {:path []}))))
(om/root
comments-data
comment-box
(.getElementById js/document "content"))
A nice feature is that each component gets their state provided as parameters to the function. The om/build
function takes a component creator function together with the app state and a path. In the above example the path is the index in the vector. We can just as well have a map:
(def app-state
(atom {:comments [{:author "Pete Hunt" :text "This is a comment."}
{:author "Jordan Walke" :text "This is *another* coment"}]}))
In this case the path denoting the first comment would have been [:comments 0]
.
Updates are managed in a similar fashion via om/update!
, taking the app state, a path and an update function. The following function will replace all authors name with a new one.
(defn change-author [app new-author]
(om/update! app [:comments]
(fn [comments]
(into [] (map #(assoc % :author new-author) comments)))))
In summary, I am really excited about React in general and together with Clojurescript in particular. To see what more complete examples looks like, have a look at the links I’ve collected below.
Let me know what you think about the article, general feedback or if you find any errors. I’d love to hear from you. Shoot me an email or message me on Twitter @lexicallyscoped.
References
- Functional DOM Programming Composing a functional API for DOM creation
- The Future of JavaScript MVC Frameworks David Nolen’s pretty epic introduction to React via his Om library.
- React Tutorial
- React Blog – Thinking in React Good overview on how to think about React and designing your components
- Why React Pete Hunt (@floydophone) writes a good summary on why React, albeit in this case instead of Angular.
- React: Rethinking best practices Video talking about React’s design decisions
- The entire React tutorial written in Om
- Pump Another Clojurescript library around React worth looking at. Uses hiccup style templates.
- TODO port in Clojurescript
← Go Back