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. -- UnknownTo 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]))
(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 ZiglarThe 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 WattersonHTML 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 InsleeA 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. -- PeleWe want our webpage to respond to user interaction and change. We need two things to achieve change:
- Inputs to our components.
- Something to watch and react to.
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 BoggsReactions 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 ShakespeareWe 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.