SPA with Reagent

Reagent is a react wrapper in clojurescript. It allows you to write html template in hiccup form. like this [:p "Hello"]. Compared with Om, it's lightweight and has an easy learning curve.

Here's steps to get started with reagent:

Get to know reagent component

A basic reagent component is just a clojurescript function.

(fn hello []
  [:div
   [:p "hello"]
   [:button {:on-click #(println "hi")} "Click"]])

Hiccup form is just a nested array. The 1st element is the component name. The 2nd form if it's an HashMap, it's the component's props. Otherwize, the 2nd form and the rest forms are children of the 1st form.

To put the compoennt on the dom, call the render function.

(r/render [hello] (.getElementById js/document "app"))

What if we want to do something when the component is loaded?
Reagent provides another form to write component, which is a function that return a render function.

(defn hello
  []
  (load-data)
  (fn []
    [:div
     [:p "hello"]
     [:button {:on-click #(println "hi")} "Click"]]))

What if we want to get hold onto react lifecycle methods?
Reagent also has the ultimate form to work with react lifecycle methods.

(def hello
  (r/create-class
   {:get-initial-state #()
    :component-did-mount #()
    :reagent-render (fn []
                      [:div
                       [:p "hello"]
                       [:button {:on-click #(println "hi")} "Click"]])}))

This component is just a hashmap containing all essential react lifecycle methods.

Managing state

Managing state is all that makes SPA hard to develop. Reagent provide you with react atom (ratom) that automaticly track user of state (deref) and re-render as needed.

Reactive state with atom

(defonce state (r/atom nil))
(defn counter
  []
  [:div
   [:h1 (get-in @state [:count] 0)]
   [:button {:on-click #(swap! state update-in [:count] + 1)}  "+"]
   [:button {:on-click #(swap! state update-in [:count] - 1)}  "-"]])

Now this counter component automaticly re-render when count is changed!

Reactive state with cursor

In the previous example, state is simple. When building larget scale apps, you often need to compose state and merge state under one variable. Actually, libraries like redux, re-frame even put the whole app's state together.

When you do that with atoms, there's a problem. All watchers of atom get called on changed, even if they may belong to different subtree of that atom. To work around that, reagent provide you a cursor, that can watch and mutate a subtree of an atom.

(defonce state (r/atom nil))
(defonce counter-state (r/cursor state [:count]))

(defn counter
  []
  [:div
   [:h1 (or @counter-state 0)]
   [:button {:on-click #(swap! counter-state + 1)}  "+"]
   [:button {:on-click #(swap! counter-state - 1)}  "-"]])

In this example, counter-state is a cursor, that give you access to only a subtree of state atom. Changes of other parts of state atom is not going trigger re-render of counter.

Derived state with track

Sometimes we need to derive new state from another state. track makes this possible. It takes a function as its argument.

(defonce state (r/atom nil))
(defonce counter-state (r/cursor state [:count]))
(defonce counter-state-derived (r/track #(* 100 (or @counter-state 0))))

(defn counter
  []
  [:div
   [:h1 @counter-state-derived]
   [:button {:on-click #(swap! counter-state + 1)}  "+"]
   [:button {:on-click #(swap! counter-state - 1)}  "-"]])

In this example, counter-state-derived is calculated from counter-state.

FAQ

  • Difference of track and track!
    track is lazy and track! is eager.

  • Difference of track and reaction
    track takes a function as its first argument, reaction does not

(track #(* 100 @counter-state 0))
(reaction (* 100 @counter-state 0))