Top Level Control with Redux State Management: A ClojureScript Tutorial

View all articles

Welcome back for the second exciting installment of Unearthing ClojureScript! In this post, I’m going to cover the next big step for getting serious with ClojureScript: state management—in this case, using React.

With front-end software, state management is a big deal. Out-of-the-box, there are a couple ways to handle state in React:

  • Keeping state at the top level, and passing it (or handlers for a particular piece of state) down to child components.
  • Throwing purity out of the window and having global variables or some Lovecraftian form of dependency injection.

Generally speaking, neither of these are great. Keeping state at the top level is fairly simple, but then there’s a large amount of overhead to passing down application state to every component that needs it.

By comparison, having global variables (or other naive versions of state) can result in hard-to-trace concurrency issues, leading to components not updating when you expect them to, or vice versa.

So how can this be tackled? For those of you who are familiar with React, you may have tried out Redux, a state container for JavaScript apps. You may have found this out of your own volition, boldly searching for a manageable system for maintaining state. Or you might have just stumbled across it while reading about JavaScript and other web tooling.

Regardless of how people end up looking at Redux, in my experience they generally end up with two thoughts:

  • “I feel like I have to use this because everyone says that I have to use it.”
  • “I don’t really fully understand why this is better.”

Generally speaking, Redux provides an abstraction that lets state management fit within the reactive nature of React. By offloading all of the statefulness to a system like Redux, you preserve the purity of React. Thus you’ll end up with a lot less headaches and generally something that’s a lot easier to reason about.

For Those New to Clojure

While this may not help you learn ClojureScript entirely from scratch, here I will at least recap some basic state concepts in Clojure[Script]. Feel free to skip these parts if you’re already a seasoned Clojurian!

Recall one of the Clojure basics that applies to ClojureScript as well: By default, data is immutable. This is great for developing and having guarantees that what you create at timestep N is still the same at timestep > N. ClojureScript also provides us with a convenient way to have mutable state if we need it, via the atom concept.

An atom in ClojureScript is very similar to an AtomicReference in Java: It provides a new object that locks its contents with concurrency guarantees. Just like in Java, you can place anything you like in this object—from then on, that atom will be an atomic reference to whatever you want.

Once you have your atom, you can atomically set a new value into it by using the reset! function (note the ! in the function—in the Clojure language this is often used to signify that an operation is stateful or impure).

Also note that—unlike Java—Clojure doesn’t care what you put into your atom. It could be a string, a list, or an object. Dynamic typing, baby!

(def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure

(println @my-mutable-map) ; You 'dereference' an atom using @
                          ; -> this prints {}

(reset! my-mutable-map {:hello "there"}) ; atomically set the atom
(reset! my-mutable-map "hello, there!")  ; don't forget Clojure is dynamic :)

Reagent extends this concept of an atom with its own atom. (If you’re not familiar with Reagent, check out the post before this.) This behaves identically to the ClojureScript atom, except it also triggers render events in Reagent, just like React’s in-built state store.

An example:

(ns example
  (:require [reagent.core :refer [atom]])) ; in this module, atom now refers
                                           ; to reagent's atom.

(def my-atom (atom "world!"))

(defn component
  []
  [:div
    [:span "Hello, " @my-atom]
    [:input {:type "button"
             :value "Press Me!"
             :on-click #(reset! My-atom "there!")}]])

This will show a single <div> containing a <span> saying “Hello, world!” and a button, as you might expect. Pressing that button will atomically mutate my-atom to contain "there!". That will trigger a redraw of the component, resulting in the span saying “Hello, there!” instead.

Overview of state management as handled by Redux and Reagent.

This seems simple enough for local, component-level mutation, but what if we have a more complicated application that has multiple levels of abstraction? Or if we need to share common state between multiple sub-components, and their sub-components?

A More Complicated Example

Let’s explore this with an example. Here we will be implementing a crude login page:

(ns unearthing-clojurescript.login
  (:require [reagent.core :as reagent :refer [atom]]))

;; -- STATE --

(def username (atom nil))
(def password (atom nil))

;; -- VIEW --

(defn component
  [on-login]
  [:div
   [:b "Username"]
   [:input {:type "text"
            :value @username
            :on-change #(reset! username (-> % .-target .-value))}]
   [:b "Password"]
   [:input {:type "password"
            :value @password
            :on-change #(reset! password (-> % .-target .-value))}]
   [:input {:type "button"
            :value "Login!"
            :on-click #(on-login @username @password)}]])

We will then host this login component within our main app.cljs, like so:

(ns unearthing-clojurescript.app
  (:require [unearthing-clojurescript.login :as login]))

;; -- STATE

(def token (atom nil))

;; -- LOGIC --

(defn- do-login-io
  [username password]
  (let [t (complicated-io-login-operation username password)]
    (reset! token t)))
    
;; -- VIEW --

(defn component
  []
  [:div
    [login/component do-login-io]])

The expected workflow is thus:

  1. We wait for the user to enter their username and password and hit submit.
  2. This will trigger our do-login-io function in the parent component.
  3. The do-login-io function does some I/O operation (such as logging in on a server and retrieving a token).

If this operation is blocking, then we’re already in a heap of trouble, as our application is frozen—if it’s not, then we have async to worry about!

Additionally, now we need to provide this token to all of our sub-components that want to do queries to our server. Code refactoring just got a lot harder!

Finally, our component is now no longer purely reactive—it is now complicit in managing the state of the rest of the application, triggering I/O and generally being a bit of nuisance.

ClojureScript Tutorial: Enter Redux

Redux is the magic wand that makes all of your state-based dreams come true. Properly implemented, it provides a state-sharing abstraction that is safe, fast, and easy to use.

The inner workings of Redux (and the theory behind it) are somewhat outside the scope of this article. Instead, I will dive into a working example with ClojureScript, which should hopefully go some way to demonstrating what it’s capable of!

In our context, Redux is implemented by one of the many ClojureScript libraries available; this one called re-frame. It provides a Clojure-ified wrapper around Redux which (in my opinion) makes it an absolute delight to use.

The Basics

Redux hoists out your application state, leaving your components lightweight. A Reduxified component only needs to think about:

  • What it looks like
  • What data it consumes
  • What events it triggers

The rest is handled behind the scenes.

To emphasize this point, let’s Reduxify our login page above.

The Database

First things first: We need to decide what our application model is going to look like. We do this by defining the shape of our data, data which will be accessible throughout the app.

A good rule of thumb is that if the data needs to be used across multiple Redux components, or needs to be long lived (like our token will be), then it should be stored in the database. By contrast, if the data is local to the component (such as our username and password fields) then it should live as local component state and not be stored in the database.

Let’s create our database boilerplate and spec out our token:

(ns unearthing-clojurescript.state.db
  (:require [cljs.spec.alpha :as s]
            [re-frame.core :as re-frame]))

(s/def ::token string?)
(s/def ::db (s/keys :opt-un [::token]))

(def default-db
  {:token nil})

There are a few interesting points worth noting here:

  • We use Clojure’s spec library to describe how our data is supposed to look. This is especially appropriate in a dynamic language like Clojure[Script].
  • For this example, we’re only keeping track of a global token that will represent our user once they have logged in. This token is a simple string.
  • However, before the user logs in, we won’t have a token. This is represented by the :opt-un keyword, which stands for “optional, unqualified.” (In Clojure, a regular keyword would be something like :cat, while a qualified keyword might be something like :animal/cat. Qualifying normally takes place at the module level—this stops keywords in different modules from clobbering each other.)
  • Finally, we specify the default state of our database, which is how it is initialized.

At any point in time, we should be confident that the data in our database matches our spec here.

Subscriptions

Now that we have described our data model, we need to reflect how our view shows that data. We have already described what our view looks like in our Redux component—now we simply need to connect our view to our database.

With Redux, we do not access our database directly—this could result in lifecycle and concurrency issues. Instead, we register our relationship with a facet of the database through subscriptions.

A subscription tells re-frame (and Reagent) that we depend on a part of the database, and if that part is altered, then our Redux component should be re-rendered.

Subscriptions are very simple to define:

(ns unearthing-clojurescript.state.subs
  (:require [re-frame.core :refer [reg-sub]]))

(reg-sub
  :token                         ; <- the name of the subscription
  (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any
    token))                      ; args passed to the subscribe function (not used here)

Here, we register a single subscription—to the token itself. A subscription is simply the name of the subscription, and the function that extracts that item from the database. We can do whatever we want to that value, and mutate the view as much as we like here; however, in this case, we’re simply extracting the token from the database and returning it.

There is much, much more you can do with subscriptions—such as defining views on subsections of the database for a tighter scope on re-rendering—but we’ll keep it simple for now!

Events

We have our database, and we have our view into the database. Now we need to trigger some events! In this example, we have two kinds of events:

  • The pure event (having no side effect) of writing a new token into the database.
  • The I/O event (having a side effect) of going out and requesting our token through some client interaction.

We’ll start with the easy one. Re-frame even provides a function exactly for this kind of event:

(ns unearthing-clojurescript.state.events
  (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf]
            [unearthing-clojurescript.state.db :refer [default-db]]))

; our start up event that initialises the database.
; we'll trigger this in our core.cljs
(reg-event-db
  :initialise-db
  (fn [_ _]
    default-db))

; a simple event that places a token in the database
(reg-event-db
  :store-login
  (fn [db [_ token]]
    (assoc db :token token)))

Again, it’s pretty straightforward here—we’ve defined two events. The first is for initializing our database. (See how it ignores both of its arguments? We always initialize the database with our default-db!) The second is for storing our token once we’ve got it.

Notice that neither of these events have side effects—no external calls, no I/O at all! This is very important to preserve the sanctity of the holy Redux process. Do not make it impure lest you wish the wrath of Redux upon you.

Finally, we need our login event. We’ll place it under the others:

(reg-event-fx
  :login
  (fn [{:keys [db]} [_ credentials]]
    {:request-token credentials}))

(reg-fx
  :request-token
  (fn [{:keys [username password]}]
    (let [token (complicated-io-login-operation username password)]
      (rf/dispatch [:store-login token]))))

The reg-event-fx function is largely similar to reg-event-db, although there are some subtle differences.

  • The first argument is no longer just the database itself. It contains a multitude of other things that you can use for managing application state.
  • The second argument is much like in reg-event-db.
  • Rather than just returning the new db, we instead return a map which represents all of the effects (“fx”) that should happen for this event. In this case, we simply call the :request-token effect, which is defined below. One of the other valid effects is :dispatch, which simply calls another event.

Once our effect has been dispatched, our :request-token effect is called, which performs our long-running-I/O login operation. Once this is finished, it happily dispatches the result back into the event loop, thus completing the cycle!

ClojureScript Tutorial: The Final Result

So! We have defined our storage abstraction. What does the component look like now?

(ns unearthing-clojurescript.login
  (:require [reagent.core :as reagent :refer [atom]]
            [re-frame.core :as rf]))

;; -- STATE --

(def username (atom nil))
(def password (atom nil))

;; -- VIEW --

(defn component
  []
  [:div
   [:b "Username"]
   [:input {:type "text"
            :value @username
            :on-change #(reset! username (-> % .-target .-value))}]
   [:b "Password"]
   [:input {:type "password"
            :value @password
            :on-change #(reset! password (-> % .-target .-value))}]
   [:input {:type "button"
            :value "Login!"
            :on-click #(rf/dispatch [:login {:username @username 
                                             :password @password]})}]])

And our app component:

(ns unearthing-clojurescript.app
  (:require [unearthing-clojurescript.login :as login]))
   
;; -- VIEW --

(defn component
  []
  [:div
    [login/component]])

And finally, accessing our token in some remote component is as simple as:

(let [token @(rf/subscribe [:token])]
  ; ...
  )

Putting it all together:

How local state and global (Redux) state work in the login example.

No fuss, no muss.

Decoupling Components with Redux/Re-frame Means Clean State Management

Using Redux (via re-frame), we successfully decoupled our view components from the mess of state handling. Extending our state abstraction is now a piece of cake!

Redux in ClojureScript really is that easy—you have no excuse not to give it a try.

If you’re ready to get cracking, I’d recommend checking out the fantastic re-frame docs and our simple worked example. I look forward to reading your comments on this ClojureScript tutorial below. Best of luck!

Understanding the Basics

What is a Redux state?

The Redux state refers to the single store that Redux uses to manage the application state. This store is solely controlled by Redux and is not directly accessible from the application itself.

About the author

Luke Tomlin, United Kingdom
member since June 12, 2018
Luke is an experienced senior software engineer, functional programming specialist, and (metaphorical) firefighter. He has developed for many leading institutions, working in core development for both the front- and back-end, writing in Kotlin and other JVM languages. He has a broad toolkit of many languages and paradigms, primarily focusing in functional-programming (Clojure, Haskell) amongst others (Kotlin, Java, Python, JavaScript). [click to continue...]
Hiring? Meet the Top 10 Freelance Clojure Developers for Hire in November 2018

Comments

comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Luke Tomlin
Clojure Developer
Luke is an experienced senior software engineer, functional programming specialist, and (metaphorical) firefighter. He has developed for many leading institutions, working in core development for both the front- and back-end, writing in Kotlin and other JVM languages. He has a broad toolkit of many languages and paradigms, primarily focusing in functional-programming (Clojure, Haskell) amongst others (Kotlin, Java, Python, JavaScript).