Reagent deep dive part 1: Hiccup and ratoms


Today I invite you to embark with me on the grand tour of Reagent, a ClojureScript library for building web pages. I will be encouraging you to try several small exercises on this page as we go. You can change the example code provided on this page to make it do other stuff. Let me know in the comments if you get stuck at all, or have any questions. This tour is going to get down into the nitty gritty of everything UI, including the kitchen sink. I will do my best to keep things interesting.

Setup

To get good at finishing, you first need to get good at starting. -- Unknown



To use Reagent in this page we require reagent.core in our namespace declaration. Typically there will be a call to reagent/render-component to start the rendering process.


(ns my.reagent-examples
  (:require
    [reagent.core :as reagent]
    [reagent.ratom]))

(enable-console-print!)
(ns my.reagent-examples
  (:require [reagent.core :as reagent]))

(when-let [element (js/document.getElementById "app")]
  (reagent/render-component [component] element)))

Reagent works by rendering HTML to an existing element in the page. So a Reagent app consists of a bare bones HTML page (index.html) which contains an element suitable for replacement. Typically this will be something like
<div id="app"></div>

To start the rendering process we call reagent/render-component with a component and the target div.

That's it for setup, we are ready to rock and roll!

A Reagent component is a function

True success has more components than one sentence or idea can contain. -- Zig Ziglar

The central abstraction Reagent provides for building views is the component. A component is a function that produces some HTML, and components can be nested to build a full application. A Reagent component is a function that returns hiccup. Hiccup is a concise way to express HTML as data. In HTML you express a paragraph with the p tag:

<p>Hello world</p>

The hiccup equivalent is:

[:p "Hello world"]

Let's write our first component.

Example A: The "hello world" Reagent component


(defn greetings []
  [:p "Hello world"])

To create a component we define a function that returns a vector where the first element of the vector is a HTML tag keyword.

Exercise: Change Example A to return a heading <h1> tag with your name in it. You can edit the examples in this page (thanks to KLIPSE). Simply modify the code in the example boxes to complete the exercises.

More about hiccup

I think hiccup cures were really invented for the amusement of the patient's friends. -- Bill Watterson



HTML tags can contain attributes. For instance an image source is expressed as an attribute:
<img src="https://avatars1.githubusercontent.com/u/9254615?v=3&s=100">
In hiccup, we express attributes as an optional map as the second element in a hiccup vector:

Example B: An image tag with a source attribute


[:img
 {:src "https://avatars1.githubusercontent.com/u/9254615?v=3&s=150"}]

Pop quiz: What is the hiccup equivalent of
<a href="https://github.com/reagent-project/reagent">Reagent</a>?

You can nest elements in the same way that you would nest HTML tags:

[:div
 [:div "Hello world"]]

Tip: [[:div] [:div]] is not a valid Reagent component. The first element of a Reagent component must be a tag identifier or component. If you want to return 2 divs, you need to wrap them in a parent div:

[:div
 [:div]
 [:div]]

Just like other attributes, you can specify handler attributes such as onClick. The Clojure kebab-style :on-click is automatically translated to camelCase onClick for convenience.

Example C: A clickable button


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

Styles attributes are expressed as a map.

<p style="color: red; background: lightblue;">Such style!</p>

is written in hiccup as:

Example D: Inline styles

[:p
 {:style {:color "white"
          :background "darkblue"}}
  "Such style!"]

There is a shorthand for expressing class and id.

[:div#my-id.my-class1.my-class2]
is equivalent to:
[:div
 {:id "my-id"
  :className "my-class1 my-class2"}]

Generally attributes map to exactly what you would expect from HTML... but it is important to understand that onclick (no hypen or camelCase) will not work. Attributes are mapped according to the React specification of attributes. If you are having trouble describing an attribute, check the React reference page to make sure you are using the appropriate name and hyphenation. That page lists all the possible attributes you can use.

There is also an escape hatch dangerouslySetHtml. You won't need it anytime soon, but it's good to know that it exists. If there is a mapping not covered by React, or you need to render a HTML template, you can use the escape hatch to write HTML from text instead of hiccup.

Let's use our knowledge to construct a more complicated component using SVG HTML elements. All of this is regular HTML.

Example E: A more complicated component using SVG tags


(defn concentric-circles []
  [:svg {:style {:border "1px solid"
                 :background "white"
                 :width "150px"
                 :height "150px"}}
   [:circle {:r 50, :cx 75, :cy 75, :fill "green"}]
   [:circle {:r 25, :cx 75, :cy 75, :fill "blue"}]
   [:path {:stroke-width 12
           :stroke "white"
           :fill "none"
           :d "M 30,40 C 100,40 50,110 120,110"}]])

As you can see we created an SVG element containing two concentric circles, and a path stroke through them. One important thing to point out here is that our function is returning a data structure.

Exercise: Complete the Lambda symbol (λ) by adding a diagonal down path.
You can find hints in the SVG Reference.

Naturally you can also do all the boring form input related stuff too *yawn*.

Example F: A tiny form for form's sake


[:div
 [:h3 "Greetings human"]
 [:form
  {:on-submit
   (fn [e]
     (.preventDefault e)
     (js/alert
       (str "You said: " (.. e -target -elements -message -value))))}
  [:label
   "Say something:"
   [:input
    {:name "message"
     :type "text"
     :default-value "Hello"}]]
  [:input {:type "submit"}]]]

This is all just regular HTML represented in hiccup syntax.

Exercise: Add a select options list to this form containing your three favorite words.
Hint: The HTML would look like <select><option>Donut</option></select>


Nesting components

What is a fish without a river? What is a bird without a tree to nest in? -- Jay Inslee



A Reagent component is a function, so you can call it directly and it will return a result:

(greetings)

But, for reasons that will become apparent, we do not call components directly. Instead we nest components, in the same way that we nest hiccup forms:

(defn greet2 [message]
  [:div [greetings]])

The only visible difference between calling a component and nesting a component is that we surround it in square braces instead of round parenthesis.

Example G: An SVG component that nests another component


(defn many-circles []
  (into
    [:svg {:style {:border "1px solid"
                   :background "white"
                   :width "600px"
                   :height "600px"}}]
    (for [i (range 12)]
      [:g
       {:transform (str
                     "translate(300,300) "
                     "rotate(" (* i 30) ") "
                     "translate(100)")}
       [concentric-circles]])))

Here we make use of our previous component. We put 12 instances of concentric-circle into an SVG.

Exercise: Redefine concentric-circle to return a g element instead of an svg. Add an argument to take the rotation as an input. Don't forget to pass i to concentric-circles. Refactoring and composing components should feel very familiar, it's the same thing we do with any other function. Functions are a convenient way to organize view modularity to suit your tastes.

But why do we use braces instead of parenthesis? You ask a good question... The reason is efficiency. The React philosophy is that views are functions of their inputs, and that view need only be re-rendered when their inputs change. If the input arguments of a function do not change, the result is the same, so there is no point calling it. We only need to call a view component if the inputs it relies on have changed.

If our components called sub components directly, it would force them to always compute a result. We don't want that. Instead we leave the task of calling the component up to Reagent. Reagent will figure out when it needs to evaluate a component. Our job is strictly to specify the structure of the view, which we do by returning a vector. The vector we return contains the sub components, but is not forcing evaluation of them.

Tip: Remember to use [component argument] instead of (component argument) when nesting components.


Doing stuff!

Success is no accident. It is hard work, perseverance, learning, studying, sacrifice and most of all, love of what you are doing or learning to do. -- Pele





We want our webpage to respond to user interaction and change. We need two things to achieve change:
  1. Inputs to our components.
  2. Something to watch and react to.
Component inputs are just regular function inputs. The new thing that Reagent introduces is the thing to watch and react to; the reagent/atom.

Reagent atoms behave very much like a regular Clojure atom. You change them with swap! or reset! and you get their value by deref @my-atom. The special thing about a reagent/atom is that all components that deref it will be re-rendered whenever the value held by the reagent/atom changes.

Example H: A counter component that re-renders on change


(def c
  (reagent/atom 1))

(defn counter []
  [:div
   [:div "Current counter value: " @c]
   [:button
    {:disabled (>= @c 4)
     :on-click
     (fn clicked [e]
       (swap! c inc))}
    "inc"]
   [:button
    {:disabled (<= @c 1)
     :on-click
     (fn clicked [e]
       (swap! c dec))}
    "dec"]
   (into [:div] (repeat @c [concentric-circles]))])

When we click the button, the value of counter is incremented, causing the counter-component to re-render. We don't have to do anything special to get this behavior. Our function derefs the counter, so Reagent knows that it needs to re-render this component whenever counter changes.

Tip: Be careful to make sure you are using a reagent/atom, not a regular atom... A regular atom will not cause components to re-render.

We can also write conditional code.

Example I: Rendering different HTML elements with conditional logic


(let [show? (reagent/atom false)]
  (fn waldo []
    [:div
     (if @show?
       [:div
        [:h3 "You found me!"]
        [:img
         {:src "https://goo.gl/EzvMNp"
          :style {:height "320px"}}]]
       [:div
        [:h3 "Where are you now?"]
        [:img
         {:src "https://i.ytimg.com/vi/HKMlPDwmTYM/maxresdefault.jpg"
          :style {:height "320px"}}]])
     [:button
      {:on-click
       (fn [e]
         (swap! show? not))}
      (if @show? "reset" "search")]]))

Data is very flexible. One of the big wins here is that we can construct data with conditionals. We don't need an explicit template, we have all the power of Clojure to build and manipulate data.


Reactions

A positive attitude causes a chain reaction of positive thoughts, events and outcomes. It is a catalyst and it sparks extraordinary results. -- Wade Boggs


Reactions are really quite amazing. Reactions define a reagent/atom like thing as an expression. It will fire updates when any reactive value it depends on changes.

Example J: Sorting as a reaction


(def rolls (reagent/atom [1 2 3 4]))
(def sorted-rolls (reagent.ratom/reaction (sort @rolls)))

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

Here we use a reaction that depends on rolls, which calculates a new value; the sorted rolls. We make use of sorted-rolls twice, but the sort is only computed once each time rolls changes. Reactions can depend on multiple things. They are a useful mechanism for defining a data flow efficiently. They are a convenient way to define data transforms that rely on multiple sources, or that will be used in multiple contexts.

Reactions are elegant to use in small quantities. The drawback of using reactions everywhere is that too many of them can become an unsightly mess of boilerplate. My rule of thumb is to use them in moderation where there is a clear performance or expressive advantage.

This leads us to a somewhat abstract consideration. If we structure our application with a single large global reagent/atom, it may be updated from multiple sources. We wouldn't want every component updated whenever any unrelated change occurred.

Reagent offers several answers to the question of how to organize code to react to application state. Reagent has reactions, cursors, and track. I'll not cover those here beyond our discussion of reactions, because I see them as situationally useful but not generally applicable.

For handling large application state, a well thought out and popular approach is to use re-frame. You should definitely read the re-frame documentation. It provides an in depth treatment of data flow in a reactive application.

The last thing I will say on this topic is that you can go a long way with the humble reagent/atom. So get building and don't over think it.




Conclusion

Good night, good night! Parting is such sweet sorrow, that I shall say good night till it be morrow. -- William Shakespeare


We have reached the first stop of our tour! We know how to build Reagent components and effect change to our application. We learnt that Reagent's fundamental abstraction is a view component. A view component is a function that returns HTML as hiccup. The mechanism for observing and effecting change is the reagent/atom.

In part 2 of the tour we shall examine the lifecycle of a Reagent component, and see how that enables us to build a 3D Sierpinski Gasket.

15 comments:

  1. If only the minutes I spend on the web were as half as productive and fun as those I've just spent here... Thanks a lot for taking the time to share this with us!

    ReplyDelete
    Replies
    1. Hi Samuel! Thank you very much for your kind feedback. I really appreciate the encouragement. :)

      Delete
  2. Thanks very much for putting this together! Very much appreciated. I look forward to part 2.

    ReplyDelete
    Replies
    1. Hi Nando,

      You are most welcome! I had a lot of fun writing it.
      Part 2 is available now
      http://timothypratley.blogspot.com/2017/01/reagent-deep-dive-part-2-lifecycle-of.html
      Part 3 should be out on Monday (or Tuesday) :)

      Delete
  3. just read part 1 and 2 in the reverse order but this is one of the best intros ive seen to reagent thanks for putting this together

    rohit

    ReplyDelete
    Replies
    1. Thank you for the kind words, very much appreciated :)

      Delete
  4. <3 love series.
    Thanks so much!

    ReplyDelete
  5. Great post! Moving on to the rest of the series.

    One minor point: shouldn't `dangerouslySetHtml` be `dangerouslySetInnerHTML`?

    ReplyDelete
    Replies
    1. Yes you are correct. Thanks for the correction. 😃

      Delete
  6. This comment has been removed by the author.

    ReplyDelete
  7. If you keep your entire application state in a single reagent atom, do I understand that only using reactions in components would mean that you can still avoid re-rendering each component unless its particular area of interest in that application state changed, as reflected in the reaction? In other words, all of the reactions would still process when the single app state changed, but their resulting values in many cases would not change, and therefore the re-rendering would not happen. Is that about right?

    ReplyDelete
    Replies
    1. Hi mjohn5
      Yes, that is correct :)

      Delete