React on steroids with ClojureScript

React on steroids with ClojureScript

qr code
React Next
Tel Aviv - Sep 10, 2017



Hiccup


		[:h3 (str (js/Date.))]
		

JSX


<h3>{(new Date()).toString()}</h3>
		


Yehonathan Sharvit
@viebel, viebel@gmail.com

Agenda

  • ClojureScript: a pragmatic LISP transpiled into javascript
  • Hiccup: HTML as data
  • Reagent: an elegant ClojureScript interface to React
  • Going further

Who am I?

  • Yehonathan Sharvit @viebel, viebel@gmail.com, LinkedIn
  • A mathematician
  • A coder
  • A pragmatic theorist
  • A freak of interactivity
  • Founded Audyx in 2013 - an Audiology Startup with 30K LOCs in Clojurescript
  • Author of Klipse - a simple client-side code evaluator pluggable on any web page
KLIPSE
  • A Web consultant: Full-Stack, clojure, clojurescript, javascript, node.js, react
  • Blogger about functional programming at blog.klipse.tech
me

ClojureScript - historical perspective

  • 1930: Alonzo Church discovers the λ-calculus - the mathematical foundation for functional programming
  • 1958: John McCarthy invents LISP - the first Functional Programming language
  • 1995: Brendan Eich is recruited by Netscape to do "scheme (a LISP dialect) in the browser"
  • Eventually, he invents Javascript
  • 2007 - Rich Hickey invents Clojure - A pragmatic dialect of LISP on top of the JVM
  • 2011 - ClojureScript - Clojure transpiled to javascript because "Clojure rocks, Javascript reaches!"
  • 2013 - Facebook creates react.js - A functional javascript frontend framework
  • Dec 2013 - First release of reagent - A react cljs interface that is faster than react!

Clojure: a dialect of LISP

LISP is homoiconic: the syntax of the code is the same as the syntax of the data - lists.
LISP stands for LIst Processor.

Code is expressed as lists where:

  • the first element of the list is the function
  • rest of the list are the arguments to the function
(+ 1 2 3 40)

Unlike in javascript, every piece of LISP code is an expression (including if, for etc…​).

(for [i [1 2 3 4]]
  (if (odd? i)
    i
    (* i 10)))

Clojure - functions

Function definition:

(defn hello [name]
  (str "Hello " name "!"))

Function application:

(hello "React Next")

Interactivity

You can follow this presentation on your desktop, tablet, phone…​
All the code snippets are interactive!

journey

Clojure - Immutable data structures

Data structures are immutable.
You cannot change an object.
You can only compute a new version of the object.

(def a {:hello "React"})
(assoc a :hello "Reagent")
a

The cool thing is that in Clojure, Immutable data structures are performant.
Read here if you want to understand this magic.

Immutability - why is it so good

Simpler to reason.
Kills lots of bugs before they arise.

Great performance in react-like apps (shouldComponentUpdate).
With immutable data, comparing two data structures is done via pointer comparison (recursively).
In many cases, ClojureScript react apps are faster than javascript react apps.

Clojure - atoms and mutations

Mutations are achieved with a specific mechanism called atom.
The atom itself is not mutable but the "content" of the atom is mutable.
In order to get the "content" of an atom, you have to deref it.

(def a-atom (atom {:hello "React"}))
@a-atom

Two ways to change the content of an atom: reset! and swap!:

(reset! a-atom {:hello "Reagent"})
(swap! a-atom assoc :chalom "Next")

We can add a watcher to an atom:

(add-watch a-atom :watcher
  (fn [key atom old-state new-state]
    (prn "-- Atom Changed --")
    (prn "key" key)
    (prn "atom" atom)
    (prn "old-state" old-state)
    (prn "new-state" new-state)))

Let’s swap! again:

(swap! a-atom assoc :bonjour "Tomorrow")

Macros - the language itself is extensible

This is one of the most powerful feature of LISP languages.

(defmacro deflog [name args body]
  `(defn ~name ~args (println (str "LOG: "'~name " was called")) ~@body))

Let’s see it in action:

(my.m/deflog hello-me [name]
  (str "hello " name))

(hello-me "React Next")

Macros work particularly well in LISP because code is data (homoiconicity).
Therefore, manipulating code is usually a matter of list manipulation.

Let’s see it how the macro has been expanded:

(macroexpand-1 '(my.m/deflog hello-me [name]
  (str "hello " name)))
  • In fact, many terms of the language are just macros that a developer could have written: for, when, …​
  • If javascript had a macro system, it would have been much simpler to create JSX (or maybe a better solution)

HICCUP - HTML as data

Representing HTML in Clojure Data Structures:

  • vectors to represent elements
  • maps to represent an element’s attributes
[:div "Hello World"]
[:p
 "Hello "
 [:a {:href "https://en.wikipedia.org/wiki/World"} "World"]
 "."]

What about styles?

[:p
 "Hello "
 [:span {:style {:color "red"}} "World"]
 "."]

Hiccup also supports shorthands for classes, ids and also element squeezing with >:

[:div#foo-12.supercool "My Div"]
[:div>p>s "Nested Element"]

HICCUP vs JSX - runtime components

JSX

In JSX, when you want to chose the component at runtime,

you have to assign a capitalized variable

function HelloLang({name}){
  const components = {
    "javascript": "div",
    "clojurescript": "p"
  }
  const Component = components[name] || "s";
  return <Component> {name} </Component>;
}
<div>
  <HelloLang name={'javascript'}/>
  <HelloLang name={'clojurescript'}/>
  <HelloLang name={'rubyscript'}/>
</div>

Hiccup

In Hiccup, you are free!

(defn hello-lang [name] ;; doesn't have to be capitalized!!!
  (let [components {"javascript" "div"
                    "clojurescript" "p"}]
    [(components name "s") name]))

[:div
 [hello-lang "javascript"]
 [hello-lang "clojurescript"]
 [hello-lang "scalascript"]]

HICCUP vs JSX - if and for

JSX

In JSX, you cannot use if and for.

The reason: if and for are not expressions in javascript

<ul>
  {[...Array(5).keys()].map(i => i % 2 === 0 && <li> {i} </li>)}
</ul>

Hiccup

In Hiccup, you are free!

[:ul
 (for [i (range 5)]
  (if (even? i)
    [:li i]))]

HICCUP vs JSX - comments

Have you ever tried to comment out part of your JSX form?
In Hiccup, you can comment out any part of the expression.

No comments.

[:div
 [:strong "Hello "]
 ;[:em "World"]
 #_[:p [:em "This is "]
     [:strong "not so funny"]]]

HICCUP vs JSX: Summary

Hiccup forms are plain Clojure vectors:

  • you don’t need to learn the Hiccup syntax
  • you don’t need to write a preprocessor
  • you don’t need to write IDE plugins
  • there are no edge cases
  • you can test part of your components as plain clojure functions
  • you can parse your hiccup code
(def a "hello")
[:h3 a " world"]

is translated by Klipse into:

(def a "hello")
(r/render-component [:h3 a " world"] js/klipse-container)
;

Reagent - basic components

Pure components are created with clojure functions

(defn button [text]
  [:button
   {:on-click
    (fn [e]
      (js/alert "You pressed the button!"))}
   text])

We embed a component, like we embed html tags using the function name instead of a keyword:

[:div
 [:div "This is a button"]
 [button "Click me"]]

Reagent - ratom and state

Ratom (reagent atom) has the same interface as a clojure atom: reset!, swap! and @.
Any component that dereferences a ratom will be automatically re-rendered.

(def counter (r/atom 0))
(defn button-inc [text]
  [:button
   {:on-click
    (fn [e]
      (swap! counter inc))}
   text])
(defn counter-display []
  [:h3 "cnt: " @counter])
[:div
 [counter-display]
 [button-inc "Click me"]]

Reagent - local ratom

If we want the ratom to be local to the component,

we have to instantiate the ratom inside the component

and return a function instead of a hiccup form

(defn button-and-counter [text val]
  (let [counter (r/atom val)]
    (fn [text]
      [:div
       [:div "Counter: " @counter]
       [:button
        {:on-click
         (fn [e]
           (swap! counter inc))}
        text]])))
[:div
 [button-and-counter "Click here" 42]
 [button-and-counter "Click also here" 64]]

The rationale is:

  • The outer function is called once per component instance.
  • The inner function is called once per rendering.

More details about reagent components here.

Reagent - reactions

Reactions allow you to define a ratom as an expression of other ratoms.

Let’s say we have a growing list of numbers and we want 3 components presenting the same list:

  • in its original order
  • sorted
  • reversly sorted

Let’s create a ratom and a reaction:

(def numbers (r/atom (repeatedly 5 (partial rand-int 100))))
(def sorted-numbers (reagent.ratom/reaction (sort @numbers)))

And now let’s display the three components

(defn sorted-d20 []
  [:div
   [:button {:on-click (fn [e] (swap! numbers conj (rand-int 20)))} "Roll!"]
   [:p (str @numbers)]
    [:p (str @sorted-numbers)]
    [:p (str (reverse @sorted-numbers))]])

How does this magic happen?
How could the reaction be re-calculated when the atom’s value changes?

Macros!

(defmacro reaction [& body]
  `(reagent.ratom/make-reaction
    (fn [] ~@body)))

Reagent - fully configurable components

Sometimes, you need to get access to the React lifecycle methods: componentWillMount, shouldComponentUpdate etc…​

Read Form-3 Reagent components to learn how to do that.

Reagent - cool stuff

Let’s draw a small logo in SVG:

(defn logo []
  (let [blue "#5881d8"
        green "#63b132"]
    [:svg {:style
           {:width "150px"}}
     [:circle {:r 50, :cx 75, :cy 75, :fill blue}]
     [:circle {:r 25, :cx 75, :cy 75, :fill green}]
     [:path {:stroke-width 12
             :stroke "white"
             :fill "none"
             :d "M 30,40 C 100,40 50,110 120,110"}]]))

Now, let’s duplicate them:

[:span (repeat 5 [logo])]

Reagent - cool stuff (cont.)

Let’s arrange the logos in a circle…​

(defn circle-of [num comp]
  (into
    [:svg {:style {:border "1px solid"
                   :background "white"
                   :width "500px"
                   :height "500px"}}]
    (for [i (range num)]
      [:g
       {:transform (str
                     "translate(250,250) "
                     "rotate(" (* i (/ 360 num)) ") "
                     "translate(100)")}
       [comp]])))
[:div
 [circle-of 12 logo]
 ;[circle-of 10 (fn [] [:circle {:r 50, :cx 75, :cy 75, :fill "blue"}])]
]

Appendix - ClojureScript Javsacript Interop

interop

Going further

ClojureScript: Learning tools

Questions

questions

Meanwhile, you can give a github star to Klipse...

powered by Klipse /