React, Om, ClojureScript

Sean Grove

sean@bushi.do

Table of Contents

Browser-UI Challenges

  • Performance
    • Hand-tweaked optimizations
    • Uncontrolled mutations
  • Reuse, Sharing
    • No possible assumptions ∴ no reuse
      • Example?
    • Some attempts, like LayoutIt, DivShot, JetStrap handle layout, but no behavior or state management
    • jQuery-ui has momentum, angular?
  • Complexity
    • Scattered state
    • Uncontrolled mutations

Om Intro

Thin(ish) wrapper around React

  • ~930 LoC
  • ~800 LoC if we're being fair

Referentially Transparent UI

  • All app state stored in a single reference:
    1: (def app-state
    2:  (atom {:navbar-expanded? false
    3:         :search-field     "example"
    4:         :user             {:id 10
    5:                            :name "Natsume Soseki"}}))
    
  • And then passed to user-defined components
     1: (ns om-intro.components
     2:     (:require [om.core :as om]
     3:               [om.dom :as dom]))
     4: 
     5: (defn comment-box [app]
     6:   (om/component
     7:     (dom/div #js {:className "comment-box"}
     8:       (dom/h1 nil "Leave a comment")
     9:       (dom/h3 nil (get-in app [:user :name]))
    10:       (dom/text {:id "new-comment"}))))
    11: 
    12: (om/root
    13:   comments-box app-state
    14:   {:target (sel1 :#comments-container)})
    
  • UI is then derived from the app state
  • The same state will always produce the same UI

1-way Data-flow

(om/build comments-box app-state)
1: <div id='comments-container'>
2:   <div class='comment-box'>
3:     <h1>Leave a comment</h1>
4:     <h3>Natsume Soseki</h3>
5:     <textarea id='new-comment' />
6:   </div>
7: </div>

Mutate the state to trigger a re-render

(swap! app-state assoc-in [:user :name] "Mikhail Bulgakov")
1: <div id='comments-container'>
2:   <div class='comment-box'>
3:     <h1>Leave a comment</h1>
4:     <h3>Mikhail Bulgakov</h3>
5:     <textarea id='new-comment' />
6:   </div>
7: </div>

Performance

Cursors

Laziest hard-working library:

  • State is managed via cursors
    • A way of minimizing external world a function/component has to know about
      • Think update-in: takes a path and a function, function only knows about terminal data, update-in translates diff back into origin data structure
    • Cursors wrap normal data structures, remember their path in a 'parent' data structure
    • Used along with components to determine which components need to be recalculated
  • Uses Clojure's immutable data-structures + cursors to determine if any path has changed via reference-equality check
    • "Can determine changed paths, starting from the root, in logarithmic time"
  • No state change, no re-render
  • After state change, changed paths in state are compared with dependencies in components
    • Dirty components are re-rendered (to virtual-dom)
    • Minimizes React reconcilation work
  • Even after state change and virtual-dom re-render, never re-renders actual DOM immediately. Schedules updates via requestAnimationFrame()

Consistency

  • Om avoids many concurrency issues by forcing consistency.
  • Only render method can read state from cursor. Callbacks, etc. can only access values.

Reuse

Declaratively compose your component, at the right layer of abstraction

Current state-of-the-art:

  • Mostly imperative
  • Opaque, scattered state
  • Unmanaged state transitions

Comparison: JS Dropdown

 1: <navbar>
 2:   <ul class="menu">
 3:       <li>
 4:           <a href="#">Dropdown</a>
 5:           <ul>
 6:               <li><a href="#">Some Action 1</a></li>
 7:               <li><a href="#">Some Action 2</a></li>
 8:               <li><a href="#">Some Action 3</a></li>
 9:               <li><a href="#">Some Action 4</a></li>
10:           </ul>
11:       </li>
12:   </ul>
13: </navbar>
1: $(document).ready(function() {
2:   $('.menu').dropit();
3: });
  • Declaration is deeply tied to implementation. Exact structure of children required.

Comparison: Om Dropdown

1: (om/navbar
2:   (om/build drop-down
3:             {:children [{:id :action-1 :title "Some Action 1"}
4:                         {:id :action-2 :title "Some Action 2"}
5:                         {:id :action-3 :title "Some Action 3"}
6:                         {:id :action-4 :title "Some Action 4"}]}))
  • Om allows you to work at the abstraction of your UI components
  • Still declarative
  • We don't know the implementation of drop-down, but we don't care
  • Component reuse, and very importantly, sharing becomes possible

Composition

Example of composable, reusable, components:

1: (om/build draggable-window
2:    {:title "Data Inspector"
3:     :content-com anhk/inspector
4:     :content-data (:user app-state)
5:     :content-opts {}})

Sharing

Components operate on data, so unexpectedly nice use cases come up:

  • Om-sync
  • Anhk
  • Draggable-window
  • History-player

We need a Bootstrap that operates on the component-layer: Table views, drop-downs, media players, sliders.

Real-world App: Omchaya

A client for the Kandan chat app. Source: https://github.com/sgrove/omchaya Demo: http://sgrove.github.io/omchaya/prod.html

Features

  • Reasonable mobile support
  • Composable plugin system (Thank you real data structures!)
    • Mentions
    • Emoji
    • Youtube/Vimeo/Image embed
    • /me support
    • Inline-pastie
    • RGB/Hex color embed
  • Collaborative music player and queueing system
  • Real-time narrowing search across people, media, music, and messages
  • Keybindings
  • Deep-linking

Approach

  • Rendering all done via Om/React
  • Each component sends app-logic events to router via core.async channels
  • State transition managed centrally via controller
  • Imperative/side-effects restricted to post-controller

Omchaya Flow

Demo!

Challenges

  • Minimize resorting to imperative changes / overcoming limitations of React
  • Understanding what to put in local, opaque state vs global state
  • Extensibility. Composability is there, but extensibility is tougher. Maybe extensibility comes from building the right components?

General Notes

  • Components should be given the minimum amount of data to render. Helps with performance as well, but modularity is much more important.
  • React Isolates mutation, Om helps push it out to the edges