How to use React and Om to make a Single Page Application

In today’s guest article we’re going to use React, Om and Clojure to build a Single Page Application (SPA). SPAs are a great way to provide your audience with a desktop-level experience via traditional HTML, JavaScript, and CSS. While simple SPAs are easy to build using these technologies, large and complex SPA’s frequently become challenging to develop and maintain. One of the biggest challenges in these scenarios comes from a SPA’s reliance on JavaScript. That's where Om and Clojure come in.

About React

In 2013 Facebook released React, a JavaScript library with a new vision for client-side HTML rendering. Unlike other client-side rendering engines, React maintains a “virtual DOM” that it compares to the browser’s HTML DOM. Instead of re-rendering the entire HTML DOM when the application models change, React calculates the differences between the virtual and HTML DOM and only updates the HTML DOM content that has changed.

Rendering only the differences makes React incredibly fast and efficient, allowing it to scale for handling large complex views that would perform poorly if rendered in other client-side frameworks like AngularJS or Ember.js.

Unlike AngularJS and Ember.js, React only acts as a view engine and does not implement the other components of a typical SPA framework, such as routing and model management. To build a Single Page Application (SPA) with React, it must be combined with other libraries or integrated with existing SPA frameworks, such as AngularJS. When integrated with AngularJS, React renders the view and relies on AngularJS to handle the other typical SPA responsibilities.

Clojure - a JavaScript Alternative

JavaScript is a fantastic language, and many of the proposals in ECMA Script 6 and 7 will improve JavaScript even more, but most developers find that once applications grow beyond a certain threshold, they regret the loss of compiled language features, such as static typing and refactoring. These language features help developers discover bugs earlier in the development process and help larger teams work with unfamiliar code.

To solve this issue, several groups of developers have worked to cross-compile their favorite language into JavaScript. Cross-compiling allows a developer to work in a compiled language, such as Java, Scala, and C#, where you can rely on type safety. When the code is ready for testing or debugging, a cross-compiler takes the source and produces the equivalent JavaScript for execution in a JavaScript engine.

Clojure is a Lisp-based language that compiles and executes on the JVM. With ClojureScript, Clojure can be cross-compiled to JavaScript. Developing a SPA in a language like ClojureScript allows developers to utilize Clojure’s features like static type safety and immutability when working with large or complex applications. The Om project allows a developer using ClojureScript to interface with React in a client-side application. Let’s get started using React and Om to build a SPA.

Getting Started

First we need to install some prerequisites. At a minimum you’ll need Leiningen, a JDK, and a Clojure-friendly editor. We recommend Light Table but you can even use Sublime with some plug-ins if you choose. Next, we’ll use Leiningen and a minimal Om template to start our project, mies-om. Open a command prompt and type the following:

lein new mies-om spa-tutorial

Leiningen sets up our minimal project template, but we’ll need some more modules to make a full SPA. As we mentioned, React does not have a router like a traditional SPA framework would. Here we’re going to setup Secretary as our router. We’re also going to pull in some helpers and macros to make Om programming easier, om-tools and http-kit. To make this change, edit your project.clj file to look like this:

project.clj

:dependencies [[org.clojure/clojure "1.6.0"]
               [org.clojure/clojurescript "0.0-2755"]
               [org.clojure/core.async “0.1.346.0-17112a-alpha"]
               [org.omcljs/om "0.8.8"]
               [prismatic/om-tools "0.3.11"]
               [http-kit "2.1.19"]
               [secretary “1.2.3”]]

also edit your core.cljs file to look like this:

core.cljs

(ns spa-tutorial.core
  (:require [om.core :as om :include-macros true]
            [om-tools.dom :as dom :include-macros true]
            [om-tools.core :refer-macros [defcomponent]]
            [secretary.core :as sec :include-macros true]
            [goog.events :as events]
            [goog.history.EventType :as EventType])

Now we can test our changes, from the command prompt in your “spa-tutorial” directory, execute the following:

lein cljsbuild once spa-tutorial

Leiningen will pull our dependencies and build our source, cross-compiling JavaScript to the “/out” folder in your project. If you browse this folder you will see the JavaScript output from the core.cljs file as well as our dependent libraries like Om, React, and Secretary. To see if our tutorial worked, open a browser to the index.html file. You should see a big “Hello world!” message if you’ve made the correct edits.

Let’s look at the code in more detail. In the core.cljs file you’ll see our simple React component, defined as follows:

core.cljs

(fn [app owner]
    (reify om/IRender
      (render [_]
        (dom/h1 nil (:text app)))))

This component is a function that implements the IRender interface expected in the React lifecycle. Our function when rendered will produce an H1 containing the text from our app-state atom. We initialize our app-state atom to contain “Hello world!”. Go ahead and change this a few times and run lein cljsbuild once spa-tutorial to see it change in the browser.

Adding Views

The next step for a client SPA is building our views and routes between them. In the core.cljs file, make the following edits:

core.cljs

(ns spa-tutorial.core
  (:require [om.core :as om :include-macros true]
            [om-tools.dom :as dom :include-macros true]
            [om-tools.core :refer-macros [defcomponent]]
            [secretary.core :as sec :include-macros true]
            [goog.events :as events]
            [goog.history.EventType :as EventType])
  (:import goog.History))

(enable-console-print!)

(sec/set-config! :prefix "#")

;;setup history API
(let [history (History.)
      navigation EventType/NAVIGATE]
  (goog.events/listen history
                     navigation
                     #(-> % .-token sec/dispatch!))
  (doto history (.setEnabled true)))


;;components
;;navigation-view will be shared by our three main components
(defn navigation-view [app owner]
  (reify
    om/IRender
    (render
     [_]
     (let [style {:style {:margin "10px;"}}]
       (dom/div style
                (dom/a (assoc style :href "#/")
                       "Home")
                (dom/a (assoc style :href "#/contact")
                       "Contact")
                (dom/a (assoc style :href "#/about")
                       "About"))))))
;;home page component
(defn index-page-view [app owner]
  (reify
    om/IRender
    (render
     [_]
     (dom/div
      (om/build navigation-view {})
      (dom/h1 "Index Page")))))

;;contact page component
(defn contact-page-view [app owner]
  (reify
    om/IRender
    (render
       [_]
       (dom/div
        (om/build navigation-view {})
        (dom/h1 "Contact Page")))))

;;about page component
(defn about-page-view [app owner]
  (reify
    om/IRender
    (render
     [_]
     (dom/div
      (om/build navigation-view {})
      (dom/h1 "About Page")))))


;;setup secretary routes
(sec/defroute index-page "/" []
  (om/root index-page-view
           {}
           {:target (. js/document (getElementById "app"))}))

(sec/defroute contact-page "/contact" []
  (om/root contact-page-view
           {}
           {:target (. js/document (getElementById "app"))}))

(sec/defroute about-page "/about" []
  (om/root about-page-view
           {}
           {:target (. js/document (getElementById "app"))}))

;;initialization
(defn main []
  (-> js/document
      .-location
      (set! "#/")))

(sec/dispatch! “/")

We’ve removed our simple starter component and added four new components: index-page, contact-page, about-page, and the shared component, navigation-view. These components are registered with Secretary via specific routes using the sec/defroute statement. As the client-side route changes, Secretary will overwrite the contents of React’s virtual DOM div element whose id=“app”. React notices the change and diff’s the new virtual DOM state versus the browser’s DOM and renders only the changed contents.

Notice that in our code, each React component is simply a Clojure function that implements the IRender interface. Om provides a familiar way to build HTML using methods such as dom/h1 which renders an HTML H1 element. Clojure functions can call each other as shown in the interaction with the navigation-view. This allows us to build our page with React’s familiar component composition approach, breaking up functionality into smaller, reusable components.

Conclusion

If you want to learn more about using Clojure to write SPAs, you’ll want to investigate the many tools and utilities provided by ClojureScript and Leiningen. It’s very easy to setup a Clojure REPL connected to your browser’s window so that Clojure forms can be evaluated in the REPL console and the side-effects rendered into your browser.

You can also enable reloading of your cross-compiled JavaScript via a few changes to Leiningen’s command line and using a project like Figwheel. Both of these tools make developing Single Page Applications in Clojure more productive and can help when debugging your code.

This is a guest post provided by Brett Burnett, CEO @ The BHW Group

If you enjoyed this article, please consider visiting their blog. You'll find frequent examples, guides, and comparisons of technologies, frameworks, and languages there.

The BHW Group Brett represents is an Austin-based web & mobile application development company. They have released over 300 applications using a wide array of frameworks and languages. They love tinkering with, testing, and writing about emerging and established technologies.

Published by bebraw on 2015-05-04 05:48:45

More to read

Comments