Reagent: Minimalistic React for ClojureScript

Introduction to Reagent Reagent provides a minimalistic interface between ClojureScript and React. It allows you to define efficient React components using nothing but plain ClojureScript functions and data, that describe your UI using a Hiccup-like syntax. The goal of Reagent is to make it possible to define arbitrarily complex UIs using just a couple of basic concepts, and to be fast enough by default that you rarely have to think about performance. A very basic Reagent component may look something like this: hide Example I am a component! I have bold and red text. Source ( defn simple-component [ ] [ :div [ :p "I am a component!" ] [ :p.someclass "I have " [ :strong "bold" ] [ :span { :style { :color "red" } } " and red " ] "text." ] ] ) You can build new components using other components as building blocks. Like this: hide Example I include simple-component. I am a component! I have bold and red text. Source ( defn simple-parent [ ] [ :div [ :p "I include simple-component." ] [ simple-component ] ] ) Data is passed to child components using plain old Clojure data types. Like this: hide Example Hello, world ! Source ( defn hello-component [ name ] [ :p "Hello, " name "!" ] ) ( defn say-hello [ ] [ hello-component "world" ] ) Note: In the example above, hello-component might just as well have been called as a normal Clojure function instead of as a Reagent component, i.e with parenthesis instead of square brackets. The only difference would have been performance, since ”real” Reagent components are only re-rendered when their data have changed. More advanced components though (see below) must be called with square brackets. Here is another example that shows items in a seq : hide Example Item 0

Item 1

Item 2 Here is a list: Source ( defn lister [ items ] [ :ul ( for [ item items ] ^ { :key item } [ :li "Item " item ] ) ] ) ( defn lister-user [ ] [ :div "Here is a list:" [ lister ( range 3 ) ] ] ) Note: The ^{:key item} part above isn’t really necessary in this simple example, but attaching a unique key to every item in a dynamically generated list of components is good practice, and helps React to improve performance for large lists. The key can be given either (as in this example) as meta-data, or as a :key item in the first argument to a component (if it is a map). See React’s documentation for more info.

Managing state in Reagent The easiest way to manage state in Reagent is to use Reagent’s own version of atom . It works exactly like the one in clojure.core, except that it keeps track of every time it is deref’ed. Any component that uses an atom is automagically re-rendered when its value changes. Let’s demonstrate that with a simple example: hide Example The atom click-count has value: 0 . Source ( ns example ( :require [ reagent.core :as r ] ) ) ( def click-count ( r/atom 0 ) ) ( defn counting-component [ ] [ :div "The atom " [ :code "click-count" ] " has value: " @click-count ". " [ :input { :type "button" :value "Click me!" :on-click # ( swap! click-count inc ) } ] ] ) Sometimes you may want to maintain state locally in a component. That is easy to do with an atom as well. Here is an example of that, where we call setTimeout every time the component is rendered to update a counter: hide Example Seconds Elapsed: 0 Source ( defn timer-component [ ] ( let [ seconds-elapsed ( r/atom 0 ) ] ( fn [ ] ( js/setTimeout # ( swap! seconds-elapsed inc ) 1000 ) [ :div "Seconds Elapsed: " @seconds-elapsed ] ) ) ) The previous example also uses another feature of Reagent: a component function can return another function, that is used to do the actual rendering. This function is called with the same arguments as the first one. This allows you to perform some setup of newly created components without resorting to React’s lifecycle events. By simply passing an atom around you can share state management between components, like this: hide Example The value is now: foo Change it here: Source ( ns example ( :require [ reagent.core :as r ] ) ) ( defn atom-input [ value ] [ :input { :type "text" :value @value :on-change # ( reset! value ( -> % .-target .-value ) ) } ] ) ( defn shared-state [ ] ( let [ val ( r/atom "foo" ) ] ( fn [ ] [ :div [ :p "The value is now: " @val ] [ :p "Change it here: " [ atom-input val ] ] ] ) ) ) Note: Component functions can be called with any arguments – as long as they are immutable. You could use mutable objects as well, but then you have to make sure that the component is updated when your data changes. Reagent assumes by default that two objects are equal if they are the same object.

Essential API Reagent supports most of React’s API, but there is really only one entry-point that is necessary for most applications: reagent.dom/render . It takes two arguments: a component, and a DOM node. For example, splashing the very first example all over the page would look like this: Source ( ns example ( :require [ reagent.core :as r ] ) ) ( defn simple-component [ ] [ :div [ :p "I am a component!" ] [ :p.someclass "I have " [ :strong "bold" ] [ :span { :style { :color "red" } } " and red " ] "text." ] ] ) ( defn render-simple [ ] ( rdom/render [ simple-component ] ( .-body js/document ) ) )

Putting it all together Here is a slightly less contrived example: a simple BMI calculator. Data is kept in a single reagent.core/atom : a map with height, weight and BMI as keys. hide Example BMI calculator Height: 180 cm Weight: 80 kg BMI: 24 normal Source ( ns example ( :require [ reagent.core :as r ] ) ) ( defn calc-bmi [ { :keys [ height weight bmi ] :as data } ] ( let [ h ( / height 100 ) ] ( if ( nil? bmi ) ( assoc data :bmi ( / weight ( * h h ) ) ) ( assoc data :weight ( * bmi h h ) ) ) ) ) ( def bmi-data ( r/atom ( calc-bmi { :height 180 :weight 80 } ) ) ) ( defn slider [ param value min max invalidates ] [ :input { :type "range" :value value :min min :max max :style { :width "100%" } :on-change ( fn [ e ] ( let [ new-value ( js/parseInt ( .. e -target -value ) ) ] ( swap! bmi-data ( fn [ data ] ( -> data ( assoc param new-value ) ( dissoc invalidates ) calc-bmi ) ) ) ) ) } ] ) ( defn bmi-component [ ] ( let [ { :keys [ weight height bmi ] } @bmi-data [ color diagnose ] ( cond ( < bmi 18.5 ) [ "orange" "underweight" ] ( < bmi 25 ) [ "inherit" "normal" ] ( < bmi 30 ) [ "orange" "overweight" ] :else [ "red" "obese" ] ) ] [ :div [ :h3 "BMI calculator" ] [ :div "Height: " ( int height ) "cm" [ slider :height height 100 220 :bmi ] ] [ :div "Weight: " ( int weight ) "kg" [ slider :weight weight 30 150 :bmi ] ] [ :div "BMI: " ( int bmi ) " " [ :span { :style { :color color } } diagnose ] [ slider :bmi bmi 10 50 :weight ] ] ] ) )