Reagent Presentational and 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:

A [Clojure]( to Javascript transpiler that leverages Google Closure to generate super small JS files.
React bindings for use with ClojureScript.
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:

<code>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 ""
                                                     :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"}}}})

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

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:

<code>cat > env/dev/cljs/crr/dev.cljs <<EOT
  (:require [crr.core :as crr]
            [figwheel.client :as fw]))


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

Now a minimal skeleton app including a reagent component:

<code>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")))

You can check everything is ok by running:

lein do clean, figwheel

You should see an output similar to the following:

<code>Figwheel: Cutting some fruit, just a sec ...
Figwheel: Validating the configuration found in project.clj
Figwheel: Configuration Valid :)
Figwheel: Starting server at
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.</code>

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

<code>cat > resources/public/index.html <<EOT
<!doctype html>
<meta charset="utf-8">
<title>Reagent and Re-Frame Luv</title>
<div id="app">
<p>Loading App...</p>
<script src="/js/client.js"></script>
window.onload = function () {;

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:

<code>(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")))</code>

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:

<code>(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:

<code>(defn hello-world-container []
  ;; our container
  (let [name "World 4"]
    (fn []
      [hello-world name])))</code>

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:

<code>(ns crr.core
  (:require [reagent.core :as reagent]
            [re-frame.core :as rf]))</code>

Define the initial state and add the initialisation handler:

<code>(def initial-state
  {:name "World 5"}) ; this is the map we want to initialise the app-db to.

  (fn [db _]
    (merge db initial-state)))</code>

Initialise the database:

<code>(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:

  (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:

(defn hello-world [name] ; accept name as a parameter
    [: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:

<code>(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}))


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

(defn hello-world [name] ; accept name as a parameter
    [: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.