The Re-frame Building Blocks Guide

Re-frame is a frontend framework built on top of Reagent, and hence on React. Reagent gives you a way to create Components and a lot of flexibility managing state with Reagent Atoms, but not much in the way of structure. Re-frame adds a beneficial amount of structure to your frontend app.

I have a hard task ahead. It's easy to list all of the features that a framework gives you and make it seem overly complicated. But everything in Re-frame is borne out of hard-won experience growing Reagent applications past the trivial demonstration stage. You will see similarities between Re-frame, Om Next, Redux, and the Elm Architecture. There is a remarkable consensus among these frameworks for how to architect a frontend application.

There are two ways I can approach the framework, and I can only choose one for this document. The first way is to list each feature of the framework, motivate it, and describe how to use it. You might hear someone mention a feature and you want to look up what it's for.

The second way is the inverse of the first: list each of the things you might want to do, motivate it, and describe which feature you should use. This second way is more use-case oriented. You know what you want to do, you just need to figure out how. I want to do both ways, but this document only does the first. I will write the inverse document soon. They should complement each other.

Event queue

Reagent really gives you just two things: a way to write Components with functions and a way to store state that those components can react to with Reagent Atoms. Reagent gives some great building blocks. But that's not much structure for building an app out of.

Re-frame gives you an Event Queue. A large part of building user interfaces is handling user events--button clicks, typing, mouse movements, etc. But to the user, a click is not a click. When a user clicks a "Login" button, their intent is different from when they click "Buy". Reagent Events give us a way to name the intent of the click and handle different clicks differently.

You can add an Event to the queue like so, using re-frame.core/dispatch. We will alias re-frame.core to rf:

(rf/dispatch [:buy 32343])

Events are vectors. They are named by a keyword in the first position. You can attach other data to the Event after the name. In the case above, the :buy Event needed an item id to know what the user wants to buy.

Events in the Re-frame Queue are processed one at a time. We'll talk about how to define what the Event does in the next section. Before we get to that, though, let's look at how the simple indirection of naming Events clears up our Components. Here's a Reagent Component for a buy button.

(defn buy-button [item-id]
  [:button
   {:on-click (fn [e]
                (.preventDefault e)
                (ajax/post (str "http://url.com/product/" item-id "/purchase")
                  {:on-success #(swap! app-state assoc :shopping-cart %)
                   :on-error #(swap! app-state update :errors conj)}))}
   "Buy"])

That Component does quite a lot! It defines:

  1. How to calculate the product's purchase URL
  2. Where to store the item in the cart when a response comes back
  3. How to handle a request error

There is more code for handling these chores than for rendering HTML. Re-frame's Events help you separate out concerns. If your code is about rendering HTML, it goes in a Component. If it is about interpreting the user's intent or having effects outside of the component, it goes in an Event.

Using those guidelines, let's rewrite our Component:

(defn buy-button [item-id]
  [:button
    {:on-click (fn [e]
                 (.preventDefault e)
                 (rf/dispatch [:buy item-id]))}
    "Buy"])

That's much clearer, and it follows the recommendations. A callback should only dispatch one Event if it has an effect outside of itself. Now let's look at handling that :buy Event.

Handling events

Events in the queue are handled one at a time by Re-frame. I like to think of it more like it as interpreting the intent of the user. If we name our Events well, they might become timeless. An e-commerce site, for instance, will always want to know when a user intends to buy an item. That intent might be expressed as a click, a tap, a swipe, or even some other action we can only imagine as VR becomes more commonplace. Buying, however, is unlikely to change, since it is the main activity of our domain, e-commerce.

Another thing that could change is our backend. The URL we POST to could change. Or the data we send with it. Maybe we switch from a shopping cart model to a one-click buy button. We want to isolate these backend changes from the details of the UI.

We define the Handler for an Event using re-frame.core/reg-event-fx. Out of all the Handlers registered with Re-frame, it will use the one registered for the name of the Event. That's the right decision, because the name of the Event should capture the intent and is unlikely to change. The handler defines a pure function from the data attached to the Event to the Effects we want the Event to have. We'll get to Effects and how we define them really soon. Let's stay focused on the Event Handlers right now.

Here's how we can define how to handle the :buy event.

(rf/reg-event-fx ;; register an event handler
  :buy           ;; for events with this name
  (fn [cofx [_ item-id]] ;; get the co-effects and destructure the event
    {:http-xhrio {:uri (str "http://url.com/product/" item-id "/purchase")
                  :method :post
                  :timeout 10000
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-success [:added-cart]
                  :on-failure [:notified-error]}
     :db (update-in (:db cofx) [:cart :items] conj {:item item-id})}))

We register the Event Handler for all Events with the name :buy (the first argument above). If we register a different Handler for the same Event name, the first will be replaced. We only have one Handler for any given Event. The second argument is a function. It takes two arguments: the Co-effects (we'll get to that later) and the Event vector itself. We usually destructure it right in the argument list. We can ignore the name since we know the name already (it's :buy), so we usually just put a _. But we want that item-id.

What does this function do? Well, it just returns a map. It's a pure function. It takes in data and returns data. That's the way we like it! So how does this send a message to our server to add an item to our cart? This map that we're returning describes that Effect. An Event can have multiple Effects; this one just has two, namely :http-xhrio and :db. Each Effect is a key/value pair in the map the Event Handler returns. We like that the function is pure because we can test it much more easily and independently of our server.

But what are those darn Effects? Let's look at that now.

Effects

Effects are usually low-level details. They are ajax requests, storing data in the application state Database, or outputting to the console. These are what get things done. We've already built a piece of data that tells us what Effect needs to happen. Now we need to define how to handle it. We use re-frame.core/reg-fx.

We want to do an HTTP POST request. The data describes the parameters of the request.

(rf/reg-fx       ;; register an event handler
  :http-xhrio    ;; the name is the key in the effects map
  (fn [request]  ;; we get the value (a map) we stored at that key
    (case (:method request :get)
      :post (ajax/post (:url request))
      ...)))

As you can see, the Effect Handler defines how to carry out the Effect we put in that map. A lot of code is left out of that Handler, because the details are not so relevant right now. But if you're interested, there is a library that defines the :http-xhrio effect. I find that it's pretty good for doing ajax.

That's the ajax Effect, but what about the :db Effect?

Re-frame comes with a few built-in Effects. One of them is to reset the Database to a new value (I promise we'll get to the Database really soon). It's the way you should modify the Database to store application state. In this case, we're adding the item optimistically to the cart.

Optimistically? Yes. All of the Effects from an Event happen at about the same time. So the ajax Effect will fire (sending an HTTP request). Meanwhile, the Database Effect will fire, modifying the Database. By the time the ajax response comes back, the item will already be in the cart on the client side. If there was a problem, we'll have to remove the item. But 99% of the time, there is no problem. So we're optimistic. We just add it locally, assuming it will work. This is a common web frontend technique to make the app feel more responsive to user input.

Database Events

We saw the built-in Effect for modifying the Database. It turns out, in practice, that the vast majority of Events only have the one Effect of modifying the Database (we'll get to the Database really soon, I promise). All they do is read in the current value of the Database and modify it based on the data packed inside the Event.

Because it's so common, there's a shortcut. If all you need to do is modify the Database in your event, you can create a Database Event. Let's say you want to store the name of the current user in the Database:

(rf/reg-event-db ;; notice it's a db event
  :save-name
  (fn [db [_ first-name last-name]]
    (update db :current-user assoc :first-name first-name :last-name last-name)))

You just return the new value of the Database directly and Re-frame does the rest. Just to show how much shorter and clearer it really is, check out the equivalent using reg-event-fx:

(rf/reg-event-fx
  :save-name
  (fn [{:keys [db]} [_ first-name last-name]]
    {:db (update db :current-user assoc :first-name first-name :last-name last-name)}))

Okay, so it's not that much shorter 🙂

Before we get to the Database, we've still got one thing I said we'd get back to: Co-effects.

Keeping our Event Handlers pure

So far, our Event Handlers are pure. They take the Event (pure data) and the current value of the Database (pure data) and return a map of Effects (also pure). But what if you need something more? What if you need, in your Effect, to get the current time? You could call (js/Date.) to get the current time, but then you've lost the purity of you Event Handler. You can throw your testability certificate out of the window.

Re-frame does have a solution. Every Event Handler of the longer form gets a Co-effects map as the first argument. We've already grabbed the db out of that by destructuring it (see our :save-name code above). But you can have more stuff in there. For instance, you can ask Re-frame for the current time. Or you can get stuff out of LocalStorage. Or you can make an entirely separate database that you maintain and get its current value.

Each Event Handler can specify which Co-effects it needs using a special three-argument version of reg-event-fx. Let's imagine we need the current time to send to the server for adding an item to the cart:

(rf/reg-event-fx
  :buy
  [(rf/inject-cofx :now)] ;; add the co-effects we need here.
  (fn [cofx [_ item-id]]
    {:http-xhrio {:uri (str "http://url.com/product/" item-id "/purchase")
                  :params {:time (:now cofx)} ;; use the time
                  :method :post
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-success [:added-cart]
                  :on-failure [:notified-error]}
     :db (update-in (:db cofx) [:cart :items] conj {:item item-id})}))

Notice the second argument is now a vector. We're injecting the Co-effect into this Handler. What that means is every time the Event is handled, there will be the current time in the :now key in the cofx map.

You can define your own Co-effects.

Handling Co-effects

Alright! More definitions! I bet at this point, if you're reading this straight through, you're starting to think that Re-frame is pretty complicated. There are a lot of parts. And they work together in specific ways. However, I hope I'm making it clear that each one serves a very distinct purpose and it's easy to figure out which one you need for each purpose. I hope to make that clear soon. Right now, I just want to make a good reference.

Anyway, back to defining Co-effects. Co-effects have a key and a bit of optional data that they can use. Here's how we can define the :now Co-effect (note that it's already defined as a built-in in Re-frame).

(rf/reg-cofx
  :now
  (fn [cofx _data] ;; _data unused
    (assoc cofx :now (js/Date.))))

Bam! Just cram the current time right into the cofx map. That's the same map that you'll get in your Event Handler. When you test your Events, you could easily redefine this Co-effect Handler to return a fixed time.

Just for good measure, let's say you wanted to have a temporary id associated with that item added to your cart. Remember, we're doing an optimistic insert. It could fail on the server. We'll want to know what to remove in case it does fail. So let's give our item an id which we can then relate back when we get the response from the server.

First, we'll assume we already have the id in the cofx map.

(rf/reg-event-fx
  :buy
  [(rf/inject-cofx :temp-id)] ;; we'll define this later
  (fn [cofx [_ item-id]] ;; get the co-effects and destructure the event
    {:http-xhrio {:uri (str "http://url.com/product/" item-id "/purchase")
                  :method :post
                  :timeout 10000
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-success [:added-cart (:temp-id cofx)]       ;; start using temp-id
                  :on-failure [:notified-error (:temp-id cofx)]}  ;; here, too
     :db (update-in (:db cofx) [:cart :items] conj {:item item-id
                                                    :temp-id (:temp-id cofx)})}))

We're adding the temp id to both the success and the failure Events. We'll be able to relate them back. We also store the temp id with the item in the Database.

Now we can define this Co-effect handler:

(defonce last-temp-id (atom 0))

(rf/reg-cofx
  :temp-id ;; same name
  (fn [cofx _]
    (assoc cofx :temp-id (swap! last-temp-id inc))))

We're using an atom to make sure the temp ids are unique, so we're definitely doing something impure. Data from impure sources should go in a Co-effect. We use the fact that swap! will return the new value of the atom. We increment it every time. And we store the temp id in the cofx map for the Event Handler to use. Easy!

We've talked about having effects on the outside world and getting data that is impure from the outside world. But we have only obliquely mentioned one of the biggest features of Re-frame: the application state Database.

So long and thanks for all the atoms

Reagent lets you create as many Atoms as you want to hold state. People experimented a lot with different schemes for keeping state. You could imagine as an example a system where the current user's information was held in one Atom, undo information held in another, and chat notifications held in yet another Atom.

This would totally work in a Reagent application. The nice thing is each thing is independent. Components can choose which Atoms they need to depend on and only be re-rendered when those particular Atoms change. The trouble is that you now have mutable state all over the place. You've recreated the problems of your forgotten past.

Re-frame makes the decision easy for you: you use Reagent Atoms only for component-local state. Application-global state goes in the centralized Database which it provides.

Because Re-frame has a place for application-global state, it can give you lots of convenient services around it. There is the Database Effect, :db, which we've already seen. It resets the value of the database. And the :db Co-effect gives your Event Handlers the current value of the Database. That is automatically added to all Event Handlers, because accessing the Database is so common. We've also seen the shorthand for Database Events, which don't have any effects besides modifying the Database.

The structure of your database

At some point you're going to wonder how to store stuff in your Database. Your Database is a map, so really you can put anything in it in whatever structure you want. But you usually don't know where you want it until you've experimented a little bit. Let's say on a first iteration, you put the shopping cart at the :items key at the top level. The db would look like this:

{:items [{:item-id 231}
          ... ;; other items
        ]
 ... ;; other stuff in the database
 }

But then you realize there's other stuff you need to store about the cart. You would love to move all of it to a nested map under the :cart key. Ugh! What a pain! How many places will you have to change? Will you have to go from Component to Component checking if it accesses the items? It's not so bad! Re-frame makes this kind of change much less painful than it would normally be because your Components don't reference paths in the Database directly.

Callback inferno

In Reagent, it was common to make your own centralized state using a single Reagent Atom. You could swap! whatever you needed to directly into the Atom. It was convenient, easy, and direct. However, there were problems.

Those problems wouldn't become apparent until your code grew big enough. The first problem was that the structure of your Database was defined all over the place--a little bit in each Component. Any callback function for a UI event might change something somewhere in the Database, and those callback functions were defined inline in the Components. On top of that, Components would dig out the values they needed from the Database right in their code. Components that added values to the Database were tightly coupled to Components that read those values out of the Database. Imagine the following two Components:

(defn increment-button [key]
  [:button
    {:on-click #(swap! app-state update-in [:counter key :value] inc)}
    "Increment"])
(defn counter-label [counter]
  [:span (get-in @app-state [:counter counter :value])])

Together they are very easy to understand. One button increments a nested value in the app state. The other displays it. However, imagine that they are in different files, each file with ten other Components, and you decide you need to change the location of those counters within the Database. Good luck finding all of the Components that access that exact key path ([:counter key :value]).

If you look at many Reagent Components, you'll notice a pattern: there are two kinds of Database access, writes and reads. Re-frame gives you Events for the writes and Subscriptions for the reads. So instead of looking through every Component, you only have to look through the Event handlers and Subscription definitions. That's much less code to look through and each one should be capturing the intent of what it does with a good name. Much more thought has gone into them than what typically happens with Components.

So there are only two places we have to look to make structural changes to the Database: Event Handlers and Subscriptions. We've seen Event Handlers, but what are Subscriptions?

Getting data from the Database reactively

Reagent Components will re-render when the Reagent Atoms they deref are modified. If you put all of your state in one Atom and all of your Components deref that Atom, you're going to get a lot of unnecessary re-rendering, probably to the point of slowing down your application. You don't want to do that. Instead, you want to specify exactly what data the Component needs and only re-render if that data changes.

Re-frame gives you Subscriptions to do that. Subscriptions are parts of the data from the Database. Let's say you wanted to build a component to list the shopping cart items. You could define a Subscription to give you just the items out of the Database.

(rf/reg-sub
  :cart-items
  (fn [db _]
    (:items db)))

Then you would subscribe to :cart-items in your Component and deref it to get the current value. The Component will be re-rendered only whenever the cart's items change.

(defn cart []
  (let [items (rf/subscribe [:cart-items])]
    (fn []
      [:ul
        (doall
          (for [item @items]
            [:li {:key (:name item)} (:name item)]))])))

Notice how the Component does not need to know the details of the Database structure anymore. It only needs to know the name of the Subscription and what data it returns. This means that if you change the Database structure, you should not have to change your components. You will have to change some of you Subscriptions, however.

Let's move the cart items in the Database under the :cart key.

(rf/reg-sub
  :cart-items
  (fn [db _]
    (:items (:cart db))))

The Component should still work even though the Database's structure has changed.

Reactive de-duplication

Subscriptions can do a little more than that. Let's say we have another Component that shows us a cute shopping cart icon with a number of items in our cart. We can put this in the header on all pages.

(defn cart-icon []
  (let [items (rf/subscribe [:cart-items])]
    (fn []
      [:span [:img {:src "/cart.png"}] " " (count @items)])))

Does this Component really need to know all of the data in the cart? No. It only needs the count. If we swap items in the cart, this Component will re-render, even if the count stays the same. Plus, Components are for rendering HTML, not for doing calculations. Sure, this is a simple calculation, but let's move it out of the Component and into a Subscription.

(rf/reg-sub
  :cart-count
  (fn [_]
    (rf/subscribe [:cart-items]))
  (fn [items]
    (count items)))

Now we're seeing a way to chain subscriptions together. Instead of getting data out of the database, this Subscription gets data from another Subscription. First we name the Subscription, then we say how to generate the Subscription we need. The last argument is a pure function. The first argument is the current value of the Subscription defined above it. In this case, it's all the cart items. The return value will be the current value of this Subscription, here just the count. That last function will be re-run every time the :cart-items Subscription changes.

Now we can re-write our icon Component:

(defn cart-icon []
  (let [count (rf/subscribe [:cart-count])]
    (fn []
      [:span [:img {:src "/cart.png"}] " " @count])))

To be sure, it's not that much shorter. But it will be re-rendered much less and less calculation will be done overall. And this is one of the things I like about Re-frame: it aligns de-duplication and rendering optimization. Pull more calculation into Subscriptions and your app will be faster.

Conclusions

I hope this romp through the features of Re-frame was enjoyable and educational. Re-frame gives you a way to capture the intent of UI actions (Events) and turn them into changes in the application state and the world outside of the browser (Effects). Further, it gives you a way to generate HTML based on changes to the application state (Components). You have a way to create interactive applications that present UI elements to the user, capture their intent, and change the UI based on that.

One of the things that I really like about Re-frame which I did not go much into is that it helps you organize your application. There is a place for everything you will want to do. With an easy decision, you can figure out exactly where to put each bit of your code. We'll go over that in another guide.

Another thing I like about it I did go over: Re-frame gives you lots of places to add meaning to your code. That is, it adds names throughout. Events are named. Effects are named. Subscriptions are named. Those names give you more semantic information and also give you just the amount of indirection you want to be able to easily make changes as you maintain your application over time.

Get on the mailing list!