Reagent Container Components

Sep 3, 2016

If you’ve used React Redux you’ve likely come across the Presentation and Container Component pattern. A similar pattern can be achieved with Reagent and Re-frame. The primary motivation to use this pattern is creating a clear separation of concerns. The presentational component renders the view from it’s properties and is ideally a pure function which makes it easy to test. The container component subscribes to updates made in the re-frame app-db.

To recap here’s a few definitions:

ClojureScript
A [Clojure](https://clojure.org/) to Javascript transpiler that leverages Google Closure to generate super small JS files.
Reagent
React bindings for use with ClojureScript.
Re-frame
Reactive datastore framework for use with ClojureScript which provides similar facilities as Redux via a specialised atom called app-db.
Presentational Component
Encapsulates visual elements. Approximately the V in MVC.
Container Component
Encapsulates logical functionality such as state updates and web requests. Approximately the C in MVC.

Ok so there’s a few things you’ll need before we get started:

Creating a Minimal Project

With the above out of the way we can get started. The first thing we need to do is gather all of the dependencies. Starting with the re-frame simple project as a base the following project.clj specifies the minimum dependencies required to build our project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cat > ${WORKSPACE}/cljs-reagent-reframe/project.clj <<EOT
(defproject cljs-reagent-reframe "0.8.0"
  :dependencies [[org.clojure/clojure        "1.8.0"]
                  [org.clojure/clojurescript  "1.9.227"]
                  [reagent  "0.6.0-rc"]
                  [re-frame "0.8.0"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel  "0.5.6"]]

  :hooks [leiningen.cljsbuild]

  :profiles {:dev {:cljsbuild
                    {:builds {:client {:source-paths ["env/dev/cljs"]
                                      :compiler     {:main "crr.dev"
                                                      :asset-path "js"
                                                      :optimizations :none
                                                      :source-map true
                                                      :source-map-timestamp true}}}}}

              :prod {:cljsbuild
                    {:builds {:client {:compiler    {:optimizations :advanced
                                                      :elide-asserts true
                                                      :pretty-print false}}}}}}

  :figwheel {:repl false}

  :clean-targets ^{:protect false} ["resources/public/js"]

  :cljsbuild {:builds {:client {:source-paths ["src"]
                                :compiler     {:output-dir "resources/public/js"
                                                :output-to  "resources/public/js/client.js"}}}})
EOT

The keen observer will note a few folders in the above project need to be created:

1
2
3
4
mkdir -p ${WORKSPACE}/cljs-reagent-reframe/env/dev/cljs/crr
mkdir -p ${WORKSPACE}/cljs-reagent-reframe/resources/public/js
mkdir -p ${WORKSPACE}/cljs-reagent-reframe/src/cljs/crr
cd ${WORKSPACE}/cljs-reagent-reframe

Next up is a little bit of figwheel love for hot reloads in the browser:

1
2
3
4
5
6
7
8
9
10
cat > env/dev/cljs/crr/dev.cljs <<EOT
(ns crr.dev
  (:require [crr.core :as crr]
            [figwheel.client :as fw]))

(enable-console-print!)

(fw/start {:on-jsload crr/run
            :websocket-url "ws://localhost:3449/figwheel-ws"})
EOT

Now a minimal skeleton app including a reagent component:

1
2
3
4
5
6
7
8
9
10
11
12
cat > src/cljs/crr/core.cljs <<EOT
(ns crr.core
  (:require [reagent.core :as reagent]))

(defn hello-world []
  [:h1 "Hello World"]) ; instead of JSX, Reagent uses Hiccup which is nested vectors.

(defn ^:export run
  []
  (reagent/render [hello-world] ; hiccup that renders hello-world into page
                  (js/document.getElementById "app")))
EOT

You can check everything is ok by running:

lein do clean, figwheel

You should see an output similar to the following:

1
2
3
4
5
6
7
8
Figwheel: Cutting some fruit, just a sec ...
Figwheel: Validating the configuration found in project.clj
Figwheel: Configuration Valid :)
Figwheel: Starting server at http://0.0.0.0:3449
Figwheel: Watching build - client
Figwheel: Cleaning build - client
Compiling "resources/public/js/client.js" from ("src" "env/dev/cljs")...
Successfully compiled "resources/public/js/client.js" in 9.909 seconds.

Ok great but that doesn’t really do much. We need a web page to take advantage of the magic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > resources/public/index.html <<EOT
<!doctype html>
<head>
<meta charset="utf-8">
<title>Reagent and Re-Frame Luv</title>
</head>
<body>
<div id="app">
<p>Loading App...</p>
</div>
<script src="/js/client.js"></script>
<script>
window.onload = function () {
  crr.core.run();
}
</script>
</body>
EOT

Starting up figwheel again we can browse the newly created HTML:

lein do clean, figwheel

Once you see the JS has been successfully compiled fire up http://localhost:3449/. Boom! You should see “Hello World” displayed in your browser. If you’re not seeing “Hello World” open your browser console and check for 404’s assets and/or JavaScript errors.

Reagent Presentational Component

Now let’s convert hello-world into a presentational component with the following change to core.cljs:

1
2
3
4
5
6
7
(defn hello-world [name] ; accept name as a parameter
  [:h1 "Hello " name]) ; instead of JSX, Reagent uses Hiccup which are simply nested vectors.

(defn ^:export run
  []
  (reagent/render [hello-world "World 2"] ; hiccup that renders hello-world into page
                  (js/document.getElementById "app")))

Reagent Container Component

Now my kind reader you might be scratching your head…“thought you said there’s hot reloads!! I want a refund!!”. Well patience my friend let’s introduce the container:

1
2
3
4
5
6
7
8
9
10
(defn hello-world-container []
  ;; our container
  (let [name "World 3"]
    (fn []
      [hello-world name])))

(defn ^:export run
  []
  (reagent/render [hello-world-container ] ; hiccup that renders hello-world into page
                  (js/document.getElementById "app")))

“Dude this still isn’t auto-loading!!” Ok refresh your browser and stay with me. To reward your patience change the name string in the container as follows:

1
2
3
4
5
(defn hello-world-container []
  ;; our container
  (let [name "World 4"]
    (fn []
      [hello-world name])))

Huzzah! Live loading! That’s the reagent container and presentational container mostly done.

Re-Frame Database Initialisation

Let’s sprinkle in a side of re-frame by initialising the database (re-frame.db/app-db).

Note: This is prep work that won’t have a visible side-effect yet!

Import re-frame:

1
2
3
(ns crr.core
  (:require [reagent.core :as reagent]
            [re-frame.core :as rf]))

Define the initial state and add the initialisation handler:

1
2
3
4
5
6
7
(def initial-state
  {:name "World 5"}) ; this is the map we want to initialise the app-db to.

(rf/reg-event-db
  :initialise
  (fn [db _]
    (merge db initial-state)))

Initialise the database:

1
2
3
4
5
(defn ^:export run
      []
      (rf/dispatch-sync [:initialise])
      (reagent/render [hello-world-container ] ; hiccup that renders hello-world into page
                      (js/document.getElementById "app")))</code>

Hit refresh and you’ll still see “Hello World 4”. What you’ve likely noticed is that any behaviour which isn’t contained in the render scope requires a refresh of the web page.

Re-Frame Database Subscription

Next let’s add a subscription to read from the app-db:

1
2
3
4
5
6
7
8
9
10
11
(rf/reg-sub
  :name
  (fn [db arg]
    (println "pirate sayz " arg) ; you should see the subscription vector in the console
    (:name db)))

(defn hello-world-container []
  ;; our container
  (let [name (rf/subscribe [:name])] ; the subscribe takes a vector allowing subscription to a deeply nested value in app-db.
    (fn []
      [hello-world @name]))) ; note we're de-referencing the atom here</code>

Adding an Input

Ok so we’ve got it reading from the app-db. How do we write? Let’s add a simple input to the component:

1
2
3
4
(defn hello-world [name] ; accept name as a parameter
  [:div
    [:input {:type :text}]
    [:h1 "Hello " name]]) ; instead of JSX, Reagent uses Hiccup which are simply nested vectors.</code>

Re-Frame Dispatching

Add a handler and link it to the input as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defn update-name [db [_ new-name]] ; re-frame examples use anonymous functions. Using a named function allows for easier testing.
  (merge db {:name new-name}))

(rf/reg-event-db
  :name-changed
  update-name)

(defn change-name [evt]
  (rf/dispatch-sync [:name-changed (-> evt .-target .-value)]))

(defn hello-world [name] ; accept name as a parameter
  [:div
    [:input {:type :text :on-change change-name :value name}]
    [:h1 "Hello " name]]) ; instead of JSX, Reagent uses Hiccup which are simply nested vectors.</code>

You now have the ability to update the title via the input field. The “:value name” pair can be omitted but demonstrates two way binding.

Tips and Tricks

Some general tips I’ve found during my own development:

  • make presentational components pure functions to ease testing.
  • make handlers pure named functions to ease testing.
  • avoid subscriptions in presentational components.
  • dereference atoms outside the presentational component.
  • link ownership of values in app-db with the container using a nested map (e.g. {:container-ns {:field “Value”}}).
  • defer “DRY”-ing handlers, containers and components until you’ve implemented at least 3 clear duplicates.
  • use dispatch-sync for input fields to prevent an unusual user experience (e.g. unexpected resetting of cursor position).
  • use dispatch for buttons and selects as it’s asynchronous nature doesn’t impact user interactions.
  • prefer specific symbols for event handlers and subscriptions rather than generic ones (e.g. :address-changed over :input-changed).

Some practises others have suggested:

  • use cljs.spec to enforce structural constraints on key/values held within app-db.

tags: [ clojure ]