Background

I recently started working on a new project that has a significant UI component. I decided that this was a good opportunity to take a look at Angular and React for building the client as a single page application.

After a bit of evaluation, I decided that React was a better fit for the project. Specifically, I found the idea of the virtual DOM very appealing and its component based approach to be a good way to manage the application state.

Once I got a bit deeper into using React I found it lacking in many areas. For example, it doesn't provide an adequate solution for complex data binding and while there are a few libraries such as react-forms, I didn't find them to be a good fit for my needs.

Having heard lots of great things about Om, I decided that this might be a good time to revisit ClojureScript. While I've done some projects in ClojureScript previously, I always ended up going back to JavaScript in the end.

For me, the benefits were not enough to outweigh the maturity of JavaScript and the tooling available for it. One of the things I found to be particularly painful was debugging generated JavaScript. This problem has now been addressed by the addition of source maps.

Trying Om

As I went through Om tutorials, I found that it exposes a lot of the incidental details to the user. Having to pass nil arguments, reify protocols, and manually convert to Js using #js hints are a but a few warts that I ran into. Although, it's worth noting that the om-tools library from Prismatic address some of these issues.

Overall, I feel that Om requires a significant time investment in order to become productive. I found myself wanting a higher level of abstraction for creating UI components and tracking state between them. This led me to trying Reagent. This library provides a very intuitive model for assembling UI components and tracking their state, and you have to learn very few concepts to start using it efficiently.

Differences between Om and Reagent

Om and Reagent make different design decisions that result in different tradeoffs, each with its own strength and weaknesses. Which of these libraries is better primarily depends on the problem you're solving.

The biggest difference between Om and Reagent is that Om is highly prescriptive in regards to state management in order to ensure that components are reusable. It's an anti-pattern for Om components to manipulate the global state directly or by calling functions to do so. Instead, components are expected to communicate using core.async channels. This is done to ensure high modularity of the components. Reagent leaves this part of the design up to you and allows using a combination of global and local states as you see fit.

Om takes a data centric view of the world by being agnostic about how the data is rendered. It treats the React DOM and Om components as implementation details. This decision often results in code that's verbose and exposes incidental details to the user. These can obviously be abstracted, but Om does not aim to provide such an abstraction and you'd have to write your own helpers as seen with Prismatic and om-tools.

On the other hand, Reagent provides a standard way to define UI components using Hiccup style syntax for DOM representation. Each UI component is a data structure that represents a particular DOM element. By taking a DOM centric view of the UI, Reagent makes writing composable UI components simple and intuitive. The resulting code is extremely succinct and highly readable. It's worth noting that nothing in the design prevents you from swapping in custom components. The only constraint is that the component must return something that is renderable.

Using Reagent

The rest of this post will walk through building a trivial Reagent app where I hope to illustrate what makes Reagent such an excellent library. Different variations of CRUD apps are probably the most common types of web applications nowadays. Let's take a look at creating a simple form with some fields that we'll want to collect and send to the server.

I won't go into details of setting up a ClojureScript project in this post, but you can use the reagent-example project to follow along. The project requires Leiningen build tool and you will need to have it installed before continuing.

Once you check out the project, you will need to start the ClojureScript compiler by running lein cljsbuild auto and run the server using lein ring server.

The app consists of UI components that are tied to a model. Whenever the user changes a value of a component, the change is reflected in our model. When the user clicks the submit button then the current state is sent to the server.

The ClojureScript code is found in the main.core under the src-cljs source directory. Let's delete its contents and start writing our application from scratch. As the first step, we'll need to reference reagent in our namespace definition.

(ns main.core
(:require [reagent.core :as reagent :refer [atom]]))

Next, let's create a Reagent component to represent the container for our page.

(defn home []
[:div
[:div.page-header [:h1 "Reagent Form"]]])

We can now render this component on the page by calling the render-component function.

(reagent/render-component [home]
(.getElementById js/document "app"))

As I mentioned above, the components can be nested inside one another. To add a text field to our form we'll write a function to represent it and add it to our home component.

Notice that even though text-input is a function we're not calling it, but instead we're putting it in a vector. The reason for this is that we're specifying the component hierarchy. The components will be run by Reagent when they need to be rendered.

We can also easily extract the row into a separate component. Once again, we won't need to call the row function directly, but can treat the component as data and leave it up to Reagent when it should be evaluated.

We now have an input field that we can display. Next, we need to create a model and bind our component to it. Reagent allows us to do this using its atom abstraction over the React state. The Reagent atoms behave just like standard Clojure atoms. The main difference is that a change in the value of the atom causes any components that dereference it to be repainted.

Any time we wish to create a local or global state we create an atom to hold it. This allows for a simple model where we can create variables for the state and observe them as they change over time. Let's add an atom to hold the state for our application and a couple of handler functions for accessing and updating it.

If we open the console, then we should see the current value of the :first-name key populated in our document whenever we click submit. We can now easily add a second component for the last name and see that it gets bound to our model in exactly the same way.

So far we've been using a global variable to hold all our state, while it's convenient for small applications this approach doesn't scale well. Fortunately, Reagent allows us to have localized states in our components. Let's take a look at implementing a multi-select component to see how this works.

When the user clicks on an item in the list, we'd like to mark it as selected. Obviously, this is something that's only relevant to the list component and shouldn't be tracked globally. All we have to do to create a local state is to initialize it in a closure.

We'll implement the multi-select by creating a component to represent the list and another to represent each selection item. The list component will accept an id and a label followed by the selection items.

Each item will be represented by a vector containing the id and the value of the item, eg: [:beer "Beer"]. The value of the list will be represented by a collection of the ids of the currently selected items.

We will use a let binding to initialize an atom with a map keyed on the item ids to represent the state of each item.

As you can see, getting started with Reagent is extremely easy and it requires very little code to create a working application. You could say that single page Reagent apps actually fit on a single page. :) In the next installment we'll take a look at using the secretary library to add client side routing to the application.