High Performance Web UI's with Om and React
LambdaJam - Brisbane, 2014
Leonardo Borges @leonardo_borges
www.leonardoborges.com www.thoughtworks.com
About‣ ThoughtWorker ‣ Functional Programming & Clojure advocate ‣ Founder of the Sydney Clojure User Group
‣ Currently writing “Clojure Reactive Programming”
Functional Programming is on the rise
And that makes us happy
However if you do client-side web development, you’re out of luck
Because Javascript…
[1,3,5].map(parseInt);!// [1, NaN, NaN]!
There are options
Today we’ll see one of them: Clojurescript
What we’ll see
‣ An Overview of React and how it enables fast rendering ‣ Meet Om, a ClojureScript interface to React ‣ Boosting React’s rendering performance with immutable data structures ‣ A simple demo featuring “data binding” and undo functionality
React
‣ Created by Facebook for building user interfaces ‣ The V in MVC ‣ Self-contained components ‣ Doesn’t make assumptions about your stack - can be used with anything
Self-contained components
‣ React combines display logic and DOM generation ‣ Components are themselves loosely coupled ‣ The whole app is re-rendered on every update ‣ Virtual DOM ‣ Efficient diff algorithm
Efficient diff algorithm
‣ Creates a virtual version of the DOM ‣ As the application state changes new DOM trees are generated ‣ React diffs the trees and computes the minimal set of changes ‣ Finally it applies the changes to the real DOM
A simple React componentvar HelloMessage = React.createClass({! displayName: 'HelloMessage',! render: function() {! return React.DOM.div(null, "Hello ", this.props.name);! }!});!!
React.renderComponent(! HelloMessage( {name:"John"} ), mountNode);
No literals?
A simple React component (using the JSX pre-processor)
var HelloMessage = React.createClass({! render: function() {! return <div>Hello {this.props.name}</div>;! }!});!!
React.renderComponent(! <HelloMessage name="John" />, mountNode);
React components are functions from application state to a DOM tree
Now let’s take a leap and look at the same component, written in Om
A simple Om component(def app-state (atom {:name "Leo"}))!!
(defn hello-message [app owner]! (reify om/IRender! (render [_]! (dom/div nil! (str "Hello " (:name app))))))!!
!
(om/root hello-message app-state! {:target (. js/document (getElementById "hello"))})!
Om/React’s component lifecycle
IWillMountIInitState IShouldUpdate
IRender
IRenderState
IShouldUpdate
‣ Called on app state changes but before rendering ‣ This is where React uses its fast diff algorithm ‣ Om components implement the fastest algorithm possible: a simple reference equality check ‣ Generally, you won’t have to implement this
IInitState & IRenderState
‣ Initialise component local state using IInitState ‣ Use IRenderState to work with it and render the component
IInitState & IRenderState(defn counter [app owner]! (reify! om/IInitState! (init-state [_]! {:clicks 0})! om/IRenderState! (render-state [_ state]! (dom/div nil! (str "Clicks " (:clicks state))! (dom/button #js {:onClick! #(om/set-state! owner :clicks (inc (:clicks state)))}! "Click me!")))))!!(om/root counter (atom {})! {:target (. js/document (getElementById "app"))})!
IRender
‣ Same as IRenderState… ‣ …except it doesn’t depend on the component local state to render
IRender(def app-state (atom {:name "Leo"}))!!
(defn hello-message [app owner]! (reify om/IRender! (render [_]! (dom/div nil! (str "Hello " (:name app))))))!!
!
(om/root hello-message app-state! {:target (. js/document (getElementById "hello"))})!
A larger example
A larger example
A reusable editable component(defn editable [text owner]! (reify! om/IInitState! (init-state [_]! {:editing false})! om/IRenderState! (render-state [_ {:keys [editing]}]! (dom/li nil! (dom/span #js {:style (display (not editing))} (om/value text))! (dom/input! #js {:style (display editing)! :value (om/value text)! :onChange #(handle-change % text owner)! :onKeyPress #(when (== (.-keyCode %) 13)! (commit-change text owner))! :onBlur (fn [e] (commit-change text owner))})! (dom/button! #js {:style (display (not editing))! :onClick #(om/set-state! owner :editing true)}! "Edit")))))!
From https://github.com/swannodette/om/wiki/Basic-Tutorial
A reusable editable component(defn editable [text owner]! (reify! om/IInitState! (init-state [_]! {:editing false})! om/IRenderState! (render-state [_ {:keys [editing]}]! (dom/li nil! (dom/span #js {:style (display (not editing))} (om/value text))! (dom/input! #js {:style (display editing)! :value (om/value text)! :onChange #(handle-change % text owner)! :onKeyPress #(when (== (.-keyCode %) 13)! (commit-change text owner))! :onBlur (fn [e] (commit-change text owner))})! (dom/button! #js {:style (display (not editing))! :onClick #(om/set-state! owner :editing true)}! "Edit")))))!
From https://github.com/swannodette/om/wiki/Basic-Tutorial
A reusable editable component(defn editable [text owner]! (reify! om/IInitState! (init-state [_]! {:editing false})! om/IRenderState! (render-state [_ {:keys [editing]}]! (dom/li nil! (dom/span #js {:style (display (not editing))} (om/value text))! (dom/input! #js {:style (display editing)! :value (om/value text)! :onChange #(handle-change % text owner)! :onKeyPress #(when (== (.-keyCode %) 13)! (commit-change text owner))! :onBlur (fn [e] (commit-change text owner))})! (dom/button! #js {:style (display (not editing))! :onClick #(om/set-state! owner :editing true)}! "Edit")))))!
From https://github.com/swannodette/om/wiki/Basic-Tutorial
The speakers view(defn speakers-view [app owner]! (reify! om/IRender! (render [_]! (dom/div nil! (dom/div #js {:id "speakers"! :style #js {:float "left"}}! (dom/h2 nil "Speakers")! (dom/button #js {:onClick undo} "Undo")! (dom/button #js {:onClick reset-app-state} "Reset")! (apply dom/ul nil! (om/build-all speaker-view (speakers app)! {:shared {:app-state app}})))! (om/build speaker-details-view app)))))
This is how you build components
The Sessions view
Same deal as before
(defn sessions-view [app owner]! (reify! om/IRender! (render [_]! (dom/div #js {:id "sessions"}! (dom/h2 nil "Sessions")! (apply dom/ul nil! (map #(om/build editable %) (vals (:sessions app))))))))!
Apps can have multiple roots
(om/root speakers-view app-state! {:target (. js/document (getElementById "speakers"))})!!
(om/root sessions-view app-state! {:target (. js/document (getElementById "sessions"))})!
You can have multiple “mini-apps” inside your main app
Makes it easy to try Om in a specific section/feature
What about “undo” and “reset”?
Implementing undo(def app-state (atom speaker-data))!(def app-history (atom [@app-state]))!!
(add-watch app-state :history! (fn [_ _ _ n]! (when-not (= (last @app-history) n)! (swap! app-history conj n))! (let [c (count @app-history)]! (prn c " Saved items in app history"))))!!
(defn undo []! (when (> (count @app-history) 1)! (swap! app-history pop)! (reset! app-state (last @app-history))))!
Implementing reset
(defn reset-app-state []! (reset! app-state (first @app-history))! (reset! app-history [@app-state]))!
Om/React components are functions from state to DOM trees
With immutable data structures we can access every version of the application state
So we simply update the application state, causing the components to get re-rendered
A bit of live coding
Summary
‣ With Om, you’re not using a crippled template language, you can leverage all of Clojurescript (including other DOM libraries) ‣ Rendering and display logic are inevitably coupled. Om/React acknowledges that a bundles them in components ‣ The whole app is re-rendered on every state change, making it easier to reason about ‣ This is efficient thanks to immutable data structures
Summary
‣ Clojurescript also provides a better development experience with a powerful browser REPL much like what you’d get with Clojure on the JVM ‣ Source maps are here today ‣ Bottom line is that Clojurescript is a serious contender
References
‣ Code: https://github.com/leonardoborges/lambdajam-2014-om-talk ‣ React documentation: http://facebook.github.io/react/ ‣ Om documentation: https://github.com/swannodette/om/wiki/Documentation#build ‣ Basic Tutorial: https://github.com/swannodette/om/wiki/Basic-Tutorial
Thanks! Questions?
Leonardo Borges @leonardo_borges
www.leonardoborges.com www.thoughtworks.com