Skip to content

Instantly share code, notes, and snippets.

@pmeinhardt
Last active November 5, 2015 21:03
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmeinhardt/9911509 to your computer and use it in GitHub Desktop.
Save pmeinhardt/9911509 to your computer and use it in GitHub Desktop.
Om, React and ClojureScript

Om, React and ClojureScript

Om -- ClojureScript interface to Facebook's React

Structure

  1. Getting started with ClojureScript
  2. Intro to React
  3. Om

Getting started with ClojureScript

ClojureScript is a new compiler for Clojure that targets JavaScript. It is designed to emit JavaScript code which is compatible with the advanced compilation mode of the Google Closure optimizing compiler.

-- github.com/clojure/clojurescript

To get started, I highly recommend taking a guided tour of the ClojureScript language.

For experimenting with ClojureScript, try ClojureScript Web REPL.

More

You may also want to check out this series of ClojureScript tutorials and ClojureScript One.

ClojureScript projects mostly use Leiningen for automation.

Intro to React

React is a JavaScript library for building user interfaces. It's declarative, efficient, and extremely flexible. What's more, it works with the libraries and frameworks that you already know.

-- github.com/facebook/react

React's own summary

  • Just the UI: Lots of people use React as the V in MVC. Since React makes no assumptions about the rest of your technology stack, it's easy to try it out on a small feature in an existing project.
  • Virtual DOM: React uses a virtual DOM diff implementation for ultra-high performance. It can also render on the server using Node.js — no heavy browser DOM required.
  • Data flow: React implements one-way reactive data flow which reduces boilerplate and is easier to reason about than traditional data binding.

More

  • Components
    • React's basic building blocks are called components
    • each component knows how to render itself, i.e. markup/templates live within the component source (as opposed to most AngularJS views)
    • components can be nested of course
  • Props
    • React components can pass properties, short props, to child components
    • props can be used to adapt rendering, fill out templates: <p>{this.props.message}</p>
    • props are immutable, passed from the parent and "owned" by the parent
  • State
  • Events and Refs
    • component event handlers can be attached to elements within the JSX template code of a component: <form onSubmit={this.handleSubmit}>…</form>
    • components can refer to child elements via this.refs, names are assigned through the ref attribute: <input type="text" ref="message">
  • JSX
    • transforms from an XML-like syntax into native JavaScript
    • var link = <a href="http://facebook.github.io/react/">Hello React!</a>;
    • becomes var link = React.DOM.a({href: 'http://facebook.github.io/react/'}, 'Hello React!');
  • Reconciliation
    • "React's key design decision is to make the API seem like it re-renders the whole app on every update"
    • …however, it actually performs a virtual DOM diff, inserting, deleting and updating nodes as needed
    • this process is called reconciliation

Example outline

<html>
  <head></head>
  <body>
    <div id="my-view"></div>                            | rendering target
    <script type="text/jsx">
      /**
       * @jsx React.DOM                                 | JSX pragma
       */

      var DemoButton = React.createClass({              + create a React component

        getInitialState: function() {                   | component spec
          return {wasClicked: false};                   |
        },                                              |
        getDefaultProps: function() {}                  |
        mixins: []                                     |
        statics: {                                      |
          customMethod: function() {}                   |
        }

        componentWillMount:        function() {},       | component lifecycle methods
        componentDidMount:                             |
        componentWillReceiveProps:                     |
        shouldComponentUpdate:                         |
        componentWillUpdate:                           |
        componentDidUpdate:                            |
        componentWillUnmount:                          |

        handleClick: function() {                       | event handler
          var name = this.refs.name.getDomNode.value(); |
          alert('Hello ' + name + '!');                 |
          this.setState({wasClicked: true});            |
        },                                              |

        render: function() {                            |
          return (                                      | JSX transforms this into plain JavaScript
            <input type="text" ref="name">              |
            <button onClick={this.handleClick}>         |
              {this.props.label.toUpperCase()}          |
            </button>                                   |
            {this.state.wasClicked}                     |
          );                                            |
        }                                               |
      });                                               |

      React.renderComponent(                            + render an instance of the component
        DemoButton({label: 'Say hello!'}),              |
        document.getElementById('my-view')              |
      );                                                |
    </script>
  </body>
</html>

-- see tutorial, component specifications and lifecycle.

References

Tooling

Om

A ClojureScript interface to Facebook's React

-- github.com/swannodette/om

Motivation

TL;DR

  • immutability of Clojure(Script) data-structures enables efficient DOM diffing (see also React docs)
    • React builds virtual DOM lazily for diffing, only looking at subtrees which changed
    • shouldComponentUpdate only needs to check reference equality
    • as opposed to JavaScript, where objects and arrays are mutable
  • no setState or other subtree modifications, but efficient batched updates
  • we always re-render from the root
  • bonus: all of important app state lies in a single piece of data (atom)
    • which means we can serialize app state
    • get snapshotting and undo "for free"

Excerpt from the original post

Modifying and querying the DOM is a huge performance bottleneck, and React embraces an approach that eschews this without sacrificing expressivity. It presents a well-designed object-oriented interface, but everything underneath the hood has been crafted with the eye of a pragmatic functional programmer. It works by generating a virtual version of the DOM and, as your application state evolves, it diffs changes between the virtual DOM trees over time. It uses these diffs to make the minimal set of changes required on the real DOM so you don't have to.

When React does a diff on the virtual DOM specified by your components, there is a very critical function - shouldComponentUpdate. If this returns false, React will never compute the children of the component. That is, React builds the virtual DOM tree lazily for diffing based on what path in the tree actually changed.

As it turns out, the default shouldComponentUpdate implementation is extremely conservative, because JavaScript devs tend to mutate objects and arrays! So in order to determine if some properties of a component have changed, they have to manually walk JavaScript objects and arrays to figure this out.

Instead of using JavaScript objects, Om uses ClojureScript data structures which we know will not be changed. Because of this, we can provide a component that implements shouldComponentUpdate by doing the fastest check possible - a reference equality check. This means we can always determine if the paths changed, starting from the root, in logarithmic time.

Thus we don't need React operations like setState, which exists to support both efficient subtree updating as well as good object-oriented style. Subtree updating for Om starting from root is always lightning fast because we're just doing reference equality checks all the way down.

Also, because we always re-render from the root, batched updates are trivial to implement. We don't bother with the batched update support in React, as it's designed to handle cases we don't care about, so we can just use our own 6-line rocket fuel enhancement.

Finally, because we always have the entire state of the UI in a single piece of data, we can trivially serialize all of the important app state - we don't need to bother with serialization protocols, or making sure that everyone implements them correctly. Om UI states are always serializable, always snapshottable.

This also means that Om UIs get undo for free. You can simply snapshot any state in memory and reinstate it whenever you like. It's memory-efficient, as ClojureScript data structures work by sharing structure.

-- David Nolen, The Future of JavaScript MVC Frameworks, 17 December 2013

Minimal example

Adapted from Om Basic Tutorial

index.html

<html>
  <head></head>
  <body>
    <div id="app"></div>
    <script src="//fb.me/react-0.10.0.js"></script>
    <script src="demo.js"></script>
  </body>
</html>

demo.cljs (compiles to demo.js)

(ns demo.core
  (:require [om.core :as om :include-macros true]           ; require om.core and alias as om
            [om.dom :as dom :include-macros true]))         ; require om.dom and alias as dom

;; Initialize app state

(def app-state
  (atom
    {:contacts
     [{:first "Ben"                        :last "Bitdiddle" :email "benb@mit.edu"}
      {:first "Alyssa" :middle-initial "P" :last "Hacker"    :email "aphacker@mit.edu"}
      {:first "Eva"    :middle "Lu"        :last "Ator"      :email "eval@mit.edu"}
      {:first "Louis"                      :last "Reasoner"  :email "prolog@mit.edu"}
      {:first "Cy"     :middle-initial "D" :last "Effect"    :email "bugs@mit.edu"}
      {:first "Lem"    :middle-initial "E" :last "Tweakit"   :email "morebugs@mit.edu"}]}))

;; Helpers

(defn middle-name [{:keys [middle middle-initial]}]
  (cond
    middle (str " " middle)
    middle-initial (str " " middle-initial ".")))

(defn display-name [{:keys [first last] :as contact}]
  (str last ", " first (middle-name contact)))

;; Components

(defn contact-view [contact owner]                          ; sub-component
  (reify
    om/IRender
    (render [this]
      (dom/li nil (display-name contact)))))

(defn contacts-view [app owner]                             ; root component
  (reify
    om/IRender
    (render [this]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts app)))))))

;; Start

(om/root                                                    ; establish render loop
  contacts-view                                             ;   - top-level rendering function
  app-state                                                 ;   - watched app/root component state
  {:target (. js/document (getElementById "app"))})         ;   - specify target element and other options

Explanation

  • om.core
    • IRender, IRenderState, IWillMount, IDidMount… protocols
    • root, build, build-all
  • om.core/root
    • establishes a Om rendering loop on a specific element in the DOM
    • Om tracks global application state changes
    • when the state changes schedules re-rendering via requestAnimationFrame, see om/core.cljs
      • fallback to setTimeout for browsers which do not support requestAnimationFrame
    • ⇒ batch updates
  • om.dom
    • provides direct access to React.DOM, e.g. om.dom/div becomes React.DOM.div

Further reading

Development considerations

@pmeinhardt
Copy link
Author

For some insights on server-side rendering of pages using React, reading the following article may be a good start: http://augustl.com/blog/2014/jdk8_react_rendering_on_server/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment