September 14, 2017 · clojurescript react

Complete re-frame Tutorial

In this tutorial, we'll introduce writing clojurescript app with reagent and re-frame.

reagent is a simple wrapper around react, it makes writing pure functional component a breath.

re-frame is like a redux in clojurescript's world. It is a architectural library to make big apps scalable, easy to manage, and it works nice with reagent.

Basic reagent components

The simplest reagent component is just a clojurescript function that return an vector.

Since it's pure functional component, it does not have state. Props is passed as function arguments.

(defn hello [name]
  [:h1 "Hello, " name])

This component can be rendered with:

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

Basic event flow with dispatch subscribe reg-event-db reg-sub

(reg-sub
 :count
 (fn [db _]
   (:count db)))

(reg-event-db
 :inc-count
 (fn [db [_ n]]
   (update-in db [:count] + n)))

(defn counter []
  (let [count @(subscribe [:count])]
    [:div
     [:p (or count 0)]
     [:button {:on-click #(dispatch [:inc-count 10])} "inc"]]))

dispatch trigger the event along with its parameters.
reg-event-db process the event, return a new state, and that state is then committed to the new global state.
subscribe automatically subscribe data from the global state and re-render on change.

dispatch event on component mount

The previous event is dispatched on user's button click.
Some event can be dispatched when a component is mounted eg. a ajax request for backend data.

reagent offers the second form of component: A function which return a render function.

(defn news-list
  ""
  []
  ;; dispatch a event on component construction. This can be taken like a constructor.
  (dispatch [:fetch-news])
  (fn []
    (let [news-list @(subscribe [:news])]
    [:ul
     (for [news news-list]
       ^{:key (:id news)}
       [:li (:text news)])])))

dispatch event on props change

Let's say the previous news list need to be enhanced. We want it to render news based on category.

We can add a category props to it, so it fetch news on a specific category
and render it.

(defn news-list
  ""
  [category]
  ;; dispatch a event on component construction. This can be taken like a constructor.
  (dispatch [:fetch-news category])
  (fn [category]
    (let [news-list @(subscribe [:news category])]
    [:ul
     (for [news news-list]
       ^{:key (:id news)}
       [:li (:text news)])])))

But there's problem. If this component is updated to a new category, it won't fetch data for that new category.
You can force mounting a new one with a different key. But there's a better way. In react, componentDidUpdate life cycle method is just for this kind scenario, and reagent exposed lifecycle methods via the third form of reagent component aka. create-class method call.

(defn news-list
  [category]
  (let [news-list @(subscribe [:news category])]
    [:ul
     (for [news news-list]
       ^{:key (:id news)}
       [:li (:text news)])]))

(def news-list-wrapper
  (create-class
   {:component-did-mount
    (fn [this]
      (let [{:keys [category]} (props this)]
        (dispatch [:fetch-news category])))

    :component-will-update
    (fn [this [_ {:keys [category]}]]
      (dispatch [:fetch-news category]))

    :reagent-render
    (fn [{:keys [category]}]
      [news-list category])}))

Causing side effect with reg-event-fx reg-fx

Real world apps have to duel with all kinds of side effects. The previous reg-event-db in fact is just a side effect called db, using a bit of syntactic sugar built around reg-event-fx.

Describing side effects with reg-event-fx

Functional discipline told us that we should keep side effects to minimum and contained. So instead of causing side effects every where, we describe it. Let someone carry it out.

reg-event-fx is the way to describe side effects. After that, we use reg-fx to register a handler that will carry out the side effect.

Here's how we fire the ajax request.

(reg-event-fx
 :fetch-news
 (fn [fx [_ category]]
   ;; describe a side effect called :http
   {:http {:method :GET
           :endpoint "https://newsapi.org/v1/articles?source=cnn&sortBy=top&apiKey=a4768b32e0034b5d827a5725e26e008d"
           :cbk (fn [data]
                  (dispatch [:got-news category data]))}}))

(reg-fx
 :http
 (fn [{:keys [method endpoint cbk]}]
   ;; fire http request, let's fake it here
   (js/setTimeout (fn []
                    (cbk [{:id 1
                           :title "World peace threatened"}
                          {:id 2
                           :title "Alien invasion this weekend"}
                          {:id 3
                           :title "Autobots incoming"}]))
                  1000)))

;; this put the data inside our db, after a successful request
(reg-event-db
 :got-news
 (fn [db [_ category data]]
   (assoc-in db [:news category] data)))

(reg-sub
 :news
 (fn [db [_ category]]
   (get-in db [:news category])))

Using interceptors

reg-event-fx and reg-event-db can accept interceptors that act like middleware around effect handler.

Here's two useful interceptor technique.

Use path interceptor to avoid denormalizing db

When you use re-frame, you put every state to a single global db registry, and you access it with a unique key.

While it's possible to use unique keys for every component in a app, it's better to put state into hierarchies like as components tree. So instead of a flattened application state like this:

{
  :feed-fav-count 100
  :feed-data []
  :news-data []
  :news-comment []
}

You have a state like this:

{
  :feed {
    :data []
    :fav-count 100
  }
  :news {
    :data []
    :comment []
  }
}

The way to do it is to use path interceptor.

(reg-event-db
 :inc-count
 [(path :counter :state)] ;; we can put multiple interceptors here that will act before and after the effect handler next
 (fn [db _]
   (update-in db [:count] inc)))

;; now you need a deeper path to get the state
(reg-sub
 :count
 (fn [db _]
   (get-in db [:counter :state :count])))

Use debug interceptor to print state before and after fx

re-frame provide a useful debug interceptor that you can use to print state before and after fx.
Let's modify the previous inc-count event handler to print use debug info

(reg-event-db
 :inc-count
 [(path :counter :state)
  (when ^boolean js/goog.DEBUG debug)]
 (fn [db _]
   (update-in db [:count] inc)))

Note: This requires goog.DEBUG variable be defined in project.clj like this

:compiler {
  :closure-defines {"goog.DEBUG" true}
}

Also note, path and debug interceptors one only works with reg-event-db.

advanced reg-sub

reg-sub has an advanced form, that takes idea from reactive programming. That is to use it as an computation function over other subscription. Here I take an example from official todos example.

(reg-sub
  :visible-todos

  ;; signal function
  ;; returns a vector of two input signals
  (fn [query-v _]
    [(subscribe [:todos])
     (subscribe [:showing])])

  ;; computation function
  (fn [[todos showing] _]   ;; that 1st parameter is a 2-vector of values
    (let [filter-fn (case showing
                      :active (complement :done)
                      :done   :done
                      :all    identity)]
      (filter filter-fn todos))))

The first function does two subscriptions, and the second does computation based on results from :todos and :showing subscription. When either :todos or :showing subscription change, visible-todos yields new result.

End note

More examples are going to be added on the following subjects.

Right now, please refer to re-frame doc for more help.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket