My Background
I've been interested in ClojureScript for a few years, and I love it. I come from a web/javascript background, so I was naturally drawn to ClojureScript's functional style, immutable data structures, and minimal syntax (towards which the javascript community has been trending). I don't get to work with it in my job, so I've been looking for other ways to tinker with it.
There's a popular web development framework in ClojureScript called re-frame, and I decided to give it a try. It took a while before I got started, because their documentation is... weird. The only official documentation was the README in the git repository (though they have since migrated it all to a separate website). It talked about dominoes... and the water cycle... It was a little daunting just to read through it. Even independent tutorials were delving into the foundational principles in great depth. I felt like it should be simple to just try it out, but the apparent complexity formed a barrier to entry. That is, until I made a realization: it's just Redux!
Okay, it's not exactly the same. Redux, re-frame, the Elm Architecture - they all came out around the same time, and influenced each other's development. But as someone coming from a web/React/Redux background - as I'm sure many who investigate re-frame are - I would have liked if someone (not necessarily the main website) just said so.
I kind of see Clojure and ClojureScript like Esperanto: a language that is more of a construction than an evolution, and one that is supposedly better than all "natural" languages, easier to learn, and will unify all people and be the language to end all other languages. Unfortunately, they also share a similar lack of popularity. At any rate, just like Esperanto, few people have Clojure as their "first language" - most programmers come to it long after they've been in the business for a while.
So anyways, while I was in the weeds of weird documentation and tutorials and articles that were trying to re-explain the fundamentals of uni-directional data flow in a slightly different way, other people were out building awesome things and living their lives. Again, I don't think the official documentation is required to explain their creation in the context of another creation, but it would have been nice if someone (anyone!) just explained re-frame in terms familiar to this old web developer, perhaps giving a mapping between re-frame concepts and React/Redux concepts ("re-frame" it, if you will).
...And I guess that someone is me.
I originally set out merely to provide an account of my experience in trying re-frame. However, since I came to understand it in React/Redux terms, and given what I just said, if you have a similar background to mine, you will hopefully be able to try out re-frame after reading this article. Keep in mind, of course, that this was my first time working with it.
How to Get Started
To create a re-frame project, type in this command:
lein new re-frame my-reframe-project
It will create a folder, bootstrapped with some files (which I'll get into in a moment).
Then run:
lein dev
As someone who has dabbled with ClojureScript projects in the past, I tried lein figwheel dev
at first, and then spent an embarrassingly long time trying to understand why it didn't work. It's just lein dev
(at least, it is now).
A dev server will spin up, and you can navigate to localhost:8280
(make sure to double-check the port).
Where to Write Your Code
Next, let's look at the files that were automatically generated, contained in src/cljs/<your-project-name>/
. Remember, I'm looking at re-frame through a React/Redux lens, so I'll draw a few comparisons.
core.cljs
This is the entrypoint, similar to index.js
in most React applications. This is where code from the other files is loaded, and where the initialization happens.
db.cljs
The re-frame "database" ("db") is equivalent to the Redux state store. db.cljs
is where the initial state is defined. Not much else goes here. I suppose alternative initial databases could be added here, but I haven't needed that yet.
(def default-db
{:text-box-values {}})
events.cljs
This is where Redux "reducers" would go. Here, you define functions that listen for events (Redux "actions"), then modify the database in response to the event and its parameters. As with almost everything in re-frame, these functions must be pure.
This is also where I define effectful events which, in Redux, would be handled by one of various available middlewares. Effectful events are defined separately from regular events to keep everything as functionally pure as possible. If regular event handlers need to have some kind of side-effect, it does not produce the side effect. It returns an effectful event (which is just data), and you write a separate effect function which listens for it.
No tutorial I read has explicitly said that effects handlers are to be placed in this file, but it seemed the logical place.
; register event (specifically to modify the database)(common use case)
(re-frame/reg-event-db
::edit-text-box
(fn [db [_ box-id value]
(update-in db [:text-box-values box-id] value)))
; register effectful event (these are contrived, and frankly rather silly examples)
(re-frame/reg-event-fx
::press-enter-in-text-box
(fn [{:keys [db]} [_ box-id]]
(let [val (get-in db [:text-box-values box-id])]
(if (empty? val)
{:send-an-alert ["Not ready yet"]}
{:send-an-alert [val]}))))
; register effect (it will listen for the effectful event)
(re-frame/reg-fx
:send-an-alert
(fn [[msg]]
(.alert js/window msg)))
subs.cljs
"Subscriptions" has no equivalent in Redux. Normally, in a React component, you would simply request the entire state object, then extract the properties from it that you need. Subscriptions in re-frame are an intermediate step. Rather than requesting the whole state, you define subscriptions that return only the data that are needed by the requestor, and in the desired format. This can be very convenient if you need to preprocess some data before using it. Think of it like the staging area in git. Why do git add
before git commit
? To filter out exactly what you want to work with. Same thing here.
(re-frame/reg-sub
::get-all-text-box-values-in-one-big-list
(fn [db]
(vals (:text-box-values db))))
views.cljs
Here is where you define your components for the interface. re-frame uses Reagent for the HTML components, which itself is a ClojureScript wrapper around react.js. It uses "hiccup notation", which is just the same data structures native to ClojureScript.
(defn main-panel []
(let [all-vals (re-frame/subscribe [::subs/get-all-text-box-values-in-one-big-list])]
[:div
{:class "container"}
(map-indexed (fn [i v]
^{:key i}
[:p v]) all-vals)
[:button
{:on-click #(re-frame/dispatch [::events/clicked-big-button "args"])}
"click me!"]))
Other files
Of course, you can create other files in the same folder which contain other components or functions, so long as you require
them wherever they are needed.
I added a css file into the resources/public/
folder, and manually linked it in the resources/public/index.html
file. I have no idea if that's the "normal" way to style a re-frame app (components can of course have inline styling, and there might be a css-in-cljs option out there), but it worked for me. Incidentally, that folder (and another js/
sub-folder) is where the compiled javascript code goes after building.
My Project
I decided to use re-frame in a project I created called Microtables. It's still in its infancy, but it's online. Microtables is a minimalistic spreadsheet app, meant for quick-and-dirty calculations on series of numbers - in between a regular calculator and Excel. I already had the interface done in Reagent, but as I continued building it out, I knew I would need a more sophisticated management of state. re-frame was the perfect fit. You can take a look at the source for more realistic code examples than I showed above.
Conclusion
So how was my experience with re-frame? I liked it.
With hot code reloading, the repl, and tools like parinfer, the coding part was smooth and pleasant.
I think its architecture has a slight edge on Redux. Because the event dispatch function is global, and because of the subscriptions, I didn't need to spend as much time passing params down through different generations of component children. I also didn't need a separate middleware (like redux-observable) to handle side effects - there's one built-in. Opinionated, yes, but a first-class feature.
As I mentioned before, the documentation could use some work (which it is getting, from the looks of the new website). Maybe I'm just bad at reading documentation, but poor docs seems to be common in the Clojure community! However, I hope I've demonstrated that if you already have experience in the web frontend world, the learning curve is not very steep - neither is that of ClojureScript itself.
Lastly, the programming language itself is a huge bonus. If you're not familiar with Clojure(Script), you should check it out!
Has re-frame made frontend work fun again, or simply new again? I suppose time will tell, but so far, both! I'm really liking what I'm seeing.
Banner photo by Ryan Quintal via Unsplash