Top-level Control With Redux State Management: A ClojureScript Tutorial
ClojureScript is the tool of choice for front-end developers who have tried it. Recently in this series, we showed how to use it to get started with React. In today’s tutorial, Toptal Freelance Clojure Developer Luke Tomlin dives into how to use Redux for React state management in ClojureScript.
ClojureScript is the tool of choice for front-end developers who have tried it. Recently in this series, we showed how to use it to get started with React. In today’s tutorial, Toptal Freelance Clojure Developer Luke Tomlin dives into how to use Redux for React state management in ClojureScript.
With a Master’s in CS and mathematics, Luke specializes in functional programming. A Google internship launched his powerhouse dev career.
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.
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:
- We wait for the user to enter their username and password and hit submit.
- This will trigger our
do-login-io
function in the parent component. - 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:
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.
Is Redux event sourcing?
No, Redux is a separate technology from the pattern known as event sourcing. Redux was inspired by another technology called Flux.
What is a Redux container?
A Redux container (or simply a “container”) is a React component that subscribes to the Redux state, receiving updates when that part of the state changes.
Is Redux a framework?
Yes, Redux provides a framework around state management in a web application.
What is ClojureScript?
ClojureScript is a compiler for Clojure that targets JavaScript. It is commonly used to build web applications and libraries using the Clojure language.
About the author
With a Master’s in CS and mathematics, Luke specializes in functional programming. A Google internship launched his powerhouse dev career.