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.
- inject-cofx and reg-cofx
- dynamic subscription
- use component hierarchy to avoid dynamic subscription
Right now, please refer to re-frame doc for more help.