Clojurescript web applications with React and Reagent

React Concepts

  • Virtual DOM

  • Build complex apps using components

  • Components have immutable properties

  • Components have mutable states

Reagent - Getting Started

  • Project Dependency
[reagent "0.5.0"]
  • New figwheel project
lein new figwheel hello-world -- --reagent
  • New luminus project
lein new luminus hello-world +cljs

Defining a Component

  • Hiccup style HTML,CSS,Attributes
<div class="well">
    Look, I'm in a well!
</div>
(defn well
  [message]
  [:div.well
    message])
(reagent/render
    [well "Look, I'm in a well!"]
    (.getElementById js/document "well")))
  • Mount component in DOM

A More Comlex Component

  • HTML,CSS,Attributes
<div class="panel panel-warning">
    <div class="panel-heading">
        <h3 class="panel-title">Pending</h3>
    </div>
    <div class="panel-body">
        ...
    </div>
</div>
(defn task-panel
  []
  [:div.panel.panel-warning
   [:div.panel-heading 
    [:h3.panel-title "Pending"]]
   [:div.panel-body
    ... ]])
  • Reagent code

Composing Components

  • Function composition
(defn task-panel
  "displays tasks in a panel"
  [name task-list]
  [:div.panel.panel-warning
   [:div.panel-heading 
    [:h3.panel-title name]]
   [:div.panel-body
    (if (empty? task-list)
      [task {:name "No tasks in this state"}]
      (for [t task-list]
        ^{:key (:id t)} [task t]))]])

Composing Pages

(defn task
  []
  [:div
   [page-header "Task Management"]
   [:div.panel
    (if (empty? task-list)
      [ctask/task {:name "No tasks in task queue"}]
      (for [t task-list]
        ^{:key (:id t)} [ctask/task t]))]])
  • Function composition

Adding behaviors

  • Click and change events
  • Supported React/reagent events

Keyboard, Form, Focus,Mouse

      [:form.form-vertical
       [:fieldset
          [:input#taskName.form-control
           {:placeholder "Task Name"
            :value (:name @task-data)
            :on-change #(println "new name value:" (.-target.value %))}]
          [:input#taskDesc.form-control
           {:placeholder "Task Description"
            :value (:desc @task-data)
            :on-change #(println "new desc value:" (.-target.value %))}]
          [:a.btn.btn-info.btn-raised
           {:on-click
            (fn [e]
              (println "Add button clicked"))} "Add Task"]]

Behaviors - Local State

  • Task fields can be local states of task input component
  • Use Reagent atoms
    • Any atom value change renders component
    • Return functions
    • Use let bindings for local state
(defn new-task
  []
  (let [task-data (atom {:name nil :desc nil})] ;; local variable for new task form
    (fn []
      [:form.form-vertical
      [:input#taskName.form-control
       {:placeholder "Task Name"
        :value (:name @task-data)}] ...)

Behaviors - Local State

(defn new-task
  []
  (let [task-data (atom {:name nil :desc nil})] ;; local variable for new task form
    (fn []
      [:form.form-vertical
      [:input#taskName.form-control
       {:placeholder "Task Name"
        :value (:name @task-data)}] ...)
(defn new-task
  []
  (let [task-data (atom {:name nil :desc nil})] ;; local variable for new task form
    (fn []
      [:form.form-vertical
         ...
         [:a.btn.btn-info.btn-raised
          {:on-click (fn [e] (reset! task-data {:name nil :desc nil}))} "Add Task"]]
 ...)

Component Interactions

Form component

Panel component

  • Use core.async channels
    • Actions put messages into a channel
    • Components take messages from channel and update local component state

core.async channels

(defn new-task
  [chan-events]
  (let [task-data (atom {:name nil :desc nil})] ;; local variable for new task form
    (fn []
      [:form.form-vertical
         ...
         [:a.btn.btn-info.btn-raised
          {:on-click (fn [e]
                        (go (async/>! chan-events @task-data))
                        (reset! task-data {:name nil :desc nil}))} "Add Task"]]
 ...)

put message

core.async channels

(defn task-panel
  [name init-task-list chan-events]
  (let [task-list (atom init-task-list)]
    (go-loop []
      (let [new-task (async/<! chan-events)]
        (swap! task-list conj new-task))
      (recur))
    (fn [] 
      [:div.panel.panel-info)}
       [:div.panel-heading 
        [:h3.panel-title name]]
       [:div.panel-body
        (if (empty? @task-list)
          [task {:name "No tasks in this state"}]
          (for [t @task-list]
            ^{:key (:id t)} [task t]))]])))

take message

Putting it all together

  • Routing and application initialization
  • Application event queue
  • Reusable mixins
  • Reusable components

Application Event Queue

Component Actions

Event Queue

Services

Component Views

core.async pub/sub queue

(defn initialize-event-que
  []
  (let [chan-data (async/chan)
        event-que (async/pub chan-data :event-type)]
   ...))
(defn go-logger
  []
  (let [chan-log (async/chan)]
    (async/sub event-que :new-ui-task chan-log)
    (go-loop []
      (let [v (async/<! chan-log)]
        (println "new event: " v))
      (recur))))

Event Queue

Log Service

(defn task-panel
  [name init-task-list event-que event-chan]
  (let [task-list (atom init-task-list)
        chan-data (async/chan)]
    (async/sub event-que :service-task-update chan-data)
    (go-loop []
      (let [new-task (async/<! chan-data)]
        (swap! task-list conj new-task))
      (recur))
    (fn [] 
      [:div.panel.panel-info)}
       [:div.panel-heading {:on-click (fn [e] 
                                        (go (async/>! event-chan "click fired")))}
        [:h3.panel-title name]]
...)

Action/View

Routing and Initialization

(defroute "/" [] (session/put! :page :home))
(defroute "/task" [] (session/put! :page :task))
(defroute "/about" [] (session/put! :page :about))
...
(defonce appdata
  {:navbar
   {:brand "Tazki" 
    :items [
            {:name "Dashboard" :page :home :url "#/"}
            {:name "Task Que" :page :task :url "#/task"}
            {:name "About" :page :about :url "#/about"}]}})
  • Routing with Secretary
  • Initialization
(defn init! []
  ...
  (ev/initialize-event-que)
  (utils/mount-component cnavbar/navbar (:navbar appdata) "navbar")
  (utils/mount-component page nil "app"))

Reusable mixins

  • Functions with parameters = mixins!
  • E.g., update-panel mixin
(defn go-panel-update-mixin
  [state ref-list]
  (let [xf (filter #(= state (get-in % [:event-data :state]))) ;; transducer
        chan-data (async/chan 1 xf)]
    (async/sub event-que :service-task-update chan-data)
    (go-loop []
      (let [new-task (:event-data (async/<! chan-data))]
        (swap! ref-list conj new-task)
      (recur))))

Reusable components

  • Functions with parameters = reusable components!
(defn task-panel
  [name state init-task-list]
  (let [task-list (atom init-task-list)]
    (go-panel-update-mixin state task-list)
    (fn [] 
      [:div.panel.panel-info
       [:div.panel-heading 
        [:h3.panel-title name]]
...)

Putting it all together

  • Routing and application initialization
  • Application event queue
  • Reusable mixins
  • Reusable components

Q & A